From 2adca54b67202a951f523f81887c2b149bd4e298 Mon Sep 17 00:00:00 2001 From: developeron29 <> Date: Mon, 12 Oct 2026 18:57:22 +0530 Subject: [PATCH 1/3] feat: measuremy.site analytics --- FIRST_PARTY.md | 2 +- docs/content/docs/1.guides/2.first-party.md | 1 + .../scripts/measuremysite-analytics.md | 68 ++++++++++++ packages/script/src/registry-logos.ts | 1 + packages/script/src/registry-types.json | 20 ++++ packages/script/src/registry.ts | 16 +++ .../registry/measuremysite-analytics.ts | 41 +++++++ .../script/src/runtime/registry/schemas.ts | 8 ++ packages/script/src/runtime/types.ts | 4 +- packages/script/src/script-meta.ts | 4 + playground/nuxt.config.ts | 2 + playground/pages/index.vue | 1 + .../third-parties/measuremysite-analytics.vue | 103 ++++++++++++++++++ test/e2e-dev/first-party.test.ts | 10 ++ test/e2e/measuremysite-analytics.test.ts | 29 +++++ test/fixtures/first-party/nuxt.config.ts | 2 + test/fixtures/first-party/pages/index.vue | 1 + .../first-party/pages/measuremysite.vue | 32 ++++++ .../measuremysite-analytics/nuxt.config.ts | 11 ++ .../measuremysite-analytics/pages/index.vue | 11 ++ test/types/types.test-d.ts | 1 + test/unit/measuremysite-analytics.test.ts | 57 ++++++++++ test/unit/proxy-configs.test.ts | 8 ++ 23 files changed, 431 insertions(+), 2 deletions(-) create mode 100644 docs/content/scripts/measuremysite-analytics.md create mode 100644 packages/script/src/runtime/registry/measuremysite-analytics.ts create mode 100644 playground/pages/third-parties/measuremysite-analytics.vue create mode 100644 test/e2e/measuremysite-analytics.test.ts create mode 100644 test/fixtures/first-party/pages/measuremysite.vue create mode 100644 test/fixtures/measuremysite-analytics/nuxt.config.ts create mode 100644 test/fixtures/measuremysite-analytics/pages/index.vue create mode 100644 test/unit/measuremysite-analytics.test.ts diff --git a/FIRST_PARTY.md b/FIRST_PARTY.md index 00eb4032..976fc055 100644 --- a/FIRST_PARTY.md +++ b/FIRST_PARTY.md @@ -138,7 +138,7 @@ Four presets in `proxy-configs.ts` cover all proxy-enabled scripts: | `PRIVACY_HEATMAP` | ip, language, hardware | GA, Clarity, Hotjar | | `PRIVACY_IP_ONLY` | ip only | PostHog, Plausible, Umami, Rybbit, Databuddy, Ahrefs, Fathom, Vercel, Matomo, Carbon Ads, Lemon Squeezy, Intercom, Gravatar, YouTube, Vimeo, Calendly | -Note: Cloudflare Web Analytics, TikTok, GTM, Segment, Crisp, Mixpanel, Bing UET, SpeedCurve, Pulse, and Statable have no proxy capability, so no privacy transforms are applied. +Note: Cloudflare Web Analytics, TikTok, GTM, Segment, Crisp, Mixpanel, Bing UET, SpeedCurve, Pulse, Statable, and measuremy.site have no proxy capability, so no privacy transforms are applied. ## Script Support diff --git a/docs/content/docs/1.guides/2.first-party.md b/docs/content/docs/1.guides/2.first-party.md index 68b25092..50ab4d73 100644 --- a/docs/content/docs/1.guides/2.first-party.md +++ b/docs/content/docs/1.guides/2.first-party.md @@ -290,6 +290,7 @@ These integrations serve their main loader from your domain, but some runtime re | [Fathom](/scripts/fathom-analytics) | Fathom's bot detection rejects beacons from the server's IP, so Nuxt Scripts bundles the SDK but leaves beacons direct. | | [Pulse](/scripts/pulse-analytics) | Pulse identifies visitors from the connecting IP, so proxied beacons would merge everyone into one visitor. Nuxt Scripts bundles the tracker but leaves beacons direct. | | [Statable](/scripts/statable-analytics) | Statable identifies visitors from the connecting IP and user agent, so proxied beacons would merge everyone into one visitor. Nuxt Scripts bundles the tracker but leaves beacons direct. | +| [measuremy.site](/scripts/measuremysite-analytics) | measuremy.site identifies visitors from the connecting IP and user agent, so proxied beacons would merge everyone into one visitor. Nuxt Scripts bundles the tracker but leaves beacons direct. | | [Segment](/scripts/segment) | SDK constructs API URLs dynamically, bypassing request interception. | | [Crisp](/scripts/crisp) | SDK loads secondary scripts and CSS at runtime from `client.crisp.chat`. | | [Mixpanel](/scripts/mixpanel-analytics) | No proxy integration yet. | diff --git a/docs/content/scripts/measuremysite-analytics.md b/docs/content/scripts/measuremysite-analytics.md new file mode 100644 index 00000000..6424df51 --- /dev/null +++ b/docs/content/scripts/measuremysite-analytics.md @@ -0,0 +1,68 @@ +--- +title: measuremy.site +description: Load the measuremy.site tracker and record custom events. +links: + - label: Source + icon: i-simple-icons-github + to: https://github.com/nuxt/scripts/blob/main/packages/script/src/runtime/registry/measuremysite-analytics.ts + size: xs +--- + +[measuremy.site](https://measuremy.site/) is cookieless web analytics. The tracker is under 1 KB gzipped and stores nothing in the browser. You can read the numbers from the dashboard, or from a coding agent through its [MCP server](https://measuremy.site/docs/mcp). + +The `key` option is the site's public key (`pk_...`) from the Setup tab of the dashboard. It is safe to commit. + +::script-stats +:: + +::script-docs +:: + +## Proxying is not supported + +measuremy.site counts unique visitors from a daily salted hash of the connecting IP address and user agent. If your Nuxt server relays beacons, measuremy.site sees the server's IP instead. + +You can bundle the tracker on your Nuxt origin. Beacons still go to `https://a.measuremy.site/collect`: the composable sets `data-api` on the script tag so a bundled copy keeps the endpoint. + +## Custom events + +Use `proxy.track(name, props)` to record an event. Calls made before the tracker loads run once it is ready. Properties can be strings, numbers or booleans. + +::code-group + +```ts [Proxy] +const { proxy } = useScriptMeasuremysiteAnalytics() +function trackSignup() { + proxy.track('signup', { plan: 'pro' }) +} +``` + +```ts [onLoaded] +const { onLoaded } = useScriptMeasuremysiteAnalytics() +onLoaded(({ track }) => { + track('purchase', { plan: 'annual', amount: 99 }) +}) +``` + +:: + +Clicks and form submits can also be tracked without JavaScript, with a `data-measuremysite-event` attribute on the element. See the [install guide](https://measuremy.site/docs/install). + +## Page views in Nuxt + +The tracker counts client-side navigation without extra setup. + +::script-types +:: + +## Example + +The default trigger waits until Nuxt is ready: + +```vue [app.vue] + +``` diff --git a/packages/script/src/registry-logos.ts b/packages/script/src/registry-logos.ts index 61f53b01..6a4d2579 100644 --- a/packages/script/src/registry-logos.ts +++ b/packages/script/src/registry-logos.ts @@ -11,6 +11,7 @@ export const LOGOS = { posthog: ``, fathomAnalytics: ``, pulseAnalytics: ``, + measuremysiteAnalytics: ``, statableAnalytics: { light: ``, dark: ``, diff --git a/packages/script/src/registry-types.json b/packages/script/src/registry-types.json index edfe16f4..60f26a6f 100644 --- a/packages/script/src/registry-types.json +++ b/packages/script/src/registry-types.json @@ -947,6 +947,18 @@ "code": "export interface MatomoConsent {\n /** Push `setConsentGiven`. Requires `defaultConsent: 'required' | 'given'` at registration to have an effect. */\n give: () => void\n /** Push `forgetConsentGiven`. Requires `defaultConsent: 'required' | 'given'` at registration to have an effect. */\n forget: () => void\n}" } ], + "measuremysite-analytics": [ + { + "name": "MeasuremysiteAnalyticsOptions", + "kind": "const", + "code": "export const MeasuremysiteAnalyticsOptions = object({\n /**\n * The site's public key (`pk_...`) from the Setup tab of the measuremy.site dashboard. Safe to commit.\n * @see https://measuremy.site/docs/install\n */\n key: pipe(string(), regex(/^pk_[\\w-]+$/)),\n})" + }, + { + "name": "MeasuremysiteAnalyticsApi", + "kind": "interface", + "code": "export interface MeasuremysiteAnalyticsApi {\n /**\n * Records a custom event.\n * @param name Event name, e.g. `signup`.\n * @param props Optional properties. Strings, numbers and booleans, up to 2 KB as JSON.\n * @see https://measuremy.site/docs/install\n */\n track: (name: string, props?: Record) => void\n}" + } + ], "meta-pixel": [ { "name": "StandardEvents", @@ -3131,6 +3143,14 @@ "description": "Custom properties attached to every event from the page load, rendered as `data-statable-*` attributes on the script tag." } ], + "MeasuremysiteAnalyticsOptions": [ + { + "name": "key", + "type": "string", + "required": true, + "description": "The site's public key (`pk_...`) from the Setup tab of the measuremy.site dashboard. Safe to commit." + } + ], "ScriptGoogleMapsProps": [ { "name": "trigger", diff --git a/packages/script/src/registry.ts b/packages/script/src/registry.ts index ddff5a0f..cbbdcfdf 100644 --- a/packages/script/src/registry.ts +++ b/packages/script/src/registry.ts @@ -37,6 +37,7 @@ import { LinkedInInsightOptions, MapLibreOptions, MatomoAnalyticsOptions, + MeasuremysiteAnalyticsOptions, MetaPixelOptions, MixpanelAnalyticsOptions, NpmOptions, @@ -151,6 +152,10 @@ export const registryMeta: RegistryScriptMeta[] = [ // visitor identity, so proxied beacons would collapse every visitor into // one. Same family as Fathom (#720) and Pulse. m('statableAnalytics', 'Statable Analytics', 'analytics', 'useScriptStatableAnalytics', { bundle: true }, null), + // No proxy: measuremy.site counts unique visitors from a daily salted hash of + // the connecting IP and user agent, so proxied beacons would collapse every + // visitor into one. Same family as Fathom (#720), Pulse and Statable. + m('measuremysiteAnalytics', 'measuremy.site', 'analytics', 'useScriptMeasuremysiteAnalytics', { bundle: true }, null), // ad m('bingUet', 'Bing UET', 'ad', 'useScriptBingUet', { bundle: true, partytown: true }, null), m('metaPixel', 'Meta Pixel', 'ad', 'useScriptMetaPixel', { bundle: true, proxy: true, partytown: true }, PRIVACY_FULL), @@ -499,6 +504,17 @@ export async function registry(resolve?: (path: string) => Promise): Pro }, }, }), + def('measuremysiteAnalytics', { + schema: MeasuremysiteAnalyticsOptions, + label: 'measuremy.site', + src: 'https://a.measuremy.site/t.js', + category: 'analytics', + envDefaults: { key: '' }, + // Bundling needs no SDK patch: the tracker reads its key from its own + // script element, and the composable pins `data-api` so beacons still + // go to measuremy.site. + bundle: true, + }), // ad def('bingUet', { schema: BingUetOptions, diff --git a/packages/script/src/runtime/registry/measuremysite-analytics.ts b/packages/script/src/runtime/registry/measuremysite-analytics.ts new file mode 100644 index 00000000..cdb1bd9f --- /dev/null +++ b/packages/script/src/runtime/registry/measuremysite-analytics.ts @@ -0,0 +1,41 @@ +import type { RegistryScriptInput } from '#nuxt-scripts/types' +import { useRegistryScript } from '../utils' +import { MeasuremysiteAnalyticsOptions } from './schemas' + +export { MeasuremysiteAnalyticsOptions } + +export type MeasuremysiteAnalyticsInput = RegistryScriptInput + +export interface MeasuremysiteAnalyticsApi { + /** + * Records a custom event. + * @param name Event name, e.g. `signup`. + * @param props Optional properties. Strings, numbers and booleans, up to 2 KB as JSON. + * @see https://measuremy.site/docs/install + */ + track: (name: string, props?: Record) => void +} + +declare global { + interface Window { + measuremysite: MeasuremysiteAnalyticsApi + } +} + +export function useScriptMeasuremysiteAnalytics(_options?: MeasuremysiteAnalyticsInput) { + return useRegistryScript('measuremysiteAnalytics', options => ({ + scriptInput: { + 'src': 'https://a.measuremy.site/t.js', + 'data-key': options.key, + // The tracker posts to /collect on the origin it was served from. + // Bundling serves it from the Nuxt origin, so the endpoint is pinned here. + 'data-api': 'https://a.measuremy.site/collect', + }, + schema: import.meta.dev ? MeasuremysiteAnalyticsOptions : undefined, + scriptOptions: { + use() { + return window.measuremysite + }, + }, + }), _options) +} diff --git a/packages/script/src/runtime/registry/schemas.ts b/packages/script/src/runtime/registry/schemas.ts index ff839da5..97509905 100644 --- a/packages/script/src/runtime/registry/schemas.ts +++ b/packages/script/src/runtime/registry/schemas.ts @@ -1407,3 +1407,11 @@ export const StatableAnalyticsOptions = object({ */ props: optional(record(string(), string())), }) + +export const MeasuremysiteAnalyticsOptions = object({ + /** + * The site's public key (`pk_...`) from the Setup tab of the measuremy.site dashboard. Safe to commit. + * @see https://measuremy.site/docs/install + */ + key: pipe(string(), regex(/^pk_[\w-]+$/)), +}) diff --git a/packages/script/src/runtime/types.ts b/packages/script/src/runtime/types.ts index 24793ba5..ed374249 100644 --- a/packages/script/src/runtime/types.ts +++ b/packages/script/src/runtime/types.ts @@ -30,6 +30,7 @@ import type { LemonSqueezyInput } from './registry/lemon-squeezy' import type { LinkedInInsightInput } from './registry/linkedin-insight' import type { MapLibreInput } from './registry/maplibre' import type { MatomoAnalyticsInput } from './registry/matomo-analytics' +import type { MeasuremysiteAnalyticsInput } from './registry/measuremysite-analytics' import type { MetaPixelInput } from './registry/meta-pixel' import type { MixpanelAnalyticsInput } from './registry/mixpanel-analytics' import type { NpmInput } from './registry/npm' @@ -308,6 +309,7 @@ export interface ScriptRegistry { mixpanelAnalytics?: MixpanelAnalyticsInput rybbitAnalytics?: RybbitAnalyticsInput redditPixel?: RedditPixelInput + measuremysiteAnalytics?: MeasuremysiteAnalyticsInput segment?: SegmentInput statableAnalytics?: StatableAnalyticsInput speedcurve?: SpeedCurveInput @@ -336,7 +338,7 @@ export type BuiltInRegistryScriptKey | 'databuddyAnalytics' | 'deskcrew' | 'metaPixel' | 'fathomAnalytics' | 'instagramEmbed' | 'plausibleAnalytics' | 'googleAdsense' | 'googleAnalytics' | 'googleMaps' | 'leaflet' | 'maplibre' | 'googleRecaptcha' | 'googleSignIn' | 'lemonSqueezy' | 'googleTagManager' - | 'hotjar' | 'intercom' | 'linkedinInsight' | 'paypal' | 'posthog' | 'pulseAnalytics' | 'matomoAnalytics' + | 'hotjar' | 'intercom' | 'linkedinInsight' | 'paypal' | 'posthog' | 'pulseAnalytics' | 'matomoAnalytics' | 'measuremysiteAnalytics' | 'mixpanelAnalytics' | 'rybbitAnalytics' | 'redditPixel' | 'segment' | 'statableAnalytics' | 'stripe' | 'tawkTo' | 'tiktokPixel' | 'xEmbed' | 'xPixel' | 'snapchatPixel' | 'speedcurve' | 'youtubePlayer' | 'vercelAnalytics' | 'vimeoPlayer' | 'umamiAnalytics' | 'usercentrics' | 'gravatar' | 'npm' diff --git a/packages/script/src/script-meta.ts b/packages/script/src/script-meta.ts index f20c2fed..73874f6a 100644 --- a/packages/script/src/script-meta.ts +++ b/packages/script/src/script-meta.ts @@ -52,6 +52,10 @@ export const scriptMeta = { urls: ['https://js.ciphera.net/script.js'], trackedData: ['page-views', 'events', 'conversions', 'scrolls'], }, + measuremysiteAnalytics: { + urls: ['https://a.measuremy.site/t.js'], + trackedData: ['page-views', 'events'], + }, statableAnalytics: { urls: ['https://statable.com/js/3078194/s.js'], trackedData: ['page-views', 'events', 'conversions', 'scrolls'], diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 919a5593..3b39928c 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -38,6 +38,7 @@ export default defineNuxtConfig({ fathomAnalytics: { site: 'BRDEJWKJ' }, pulseAnalytics: { domain: 'example.com' }, statableAnalytics: { siteId: '123456' }, + measuremysiteAnalytics: { key: 'pk_test123' }, posthog: { apiKey: 'phc_CkMaDU6dr11eJoQdAiSJb1rC324dogk3T952gJ6fD9W' }, intercom: { app_id: 'akg5rmxb' }, crisp: { id: 'b1021910-7ace-425a-9ef5-07f49e5ce417' }, @@ -91,6 +92,7 @@ export default defineNuxtConfig({ fathomAnalytics: { site: 'BRDEJWKJ', trigger: 'manual' }, pulseAnalytics: { domain: 'example.com', trigger: 'manual' }, statableAnalytics: { siteId: '123456', trigger: 'manual' }, + measuremysiteAnalytics: { key: 'pk_test123', trigger: 'manual' }, cloudflareWebAnalytics: { token: 'ade278253a19413c9bd923b079870902', trigger: 'manual' }, matomoAnalytics: { matomoUrl: 'https://cdn.matomo.cloud', siteId: '1', trigger: 'manual' }, vercelAnalytics: { trigger: 'manual' }, diff --git a/playground/pages/index.vue b/playground/pages/index.vue index 30ce9ec0..18ca41ef 100644 --- a/playground/pages/index.vue +++ b/playground/pages/index.vue @@ -26,6 +26,7 @@ function getPlaygroundPath(script: any): string | null { 'rybbit-analytics': '/third-parties/rybbit-analytics', 'pulse-analytics': '/third-parties/pulse-analytics', 'statable-analytics': '/third-parties/statable-analytics', + 'measuremysite-analytics': '/third-parties/measuremysite-analytics', 'databuddy-analytics': '/third-parties/databuddy-analytics', 'umami-analytics': '/third-parties/umami-analytics', 'segment': '/third-parties/segment', diff --git a/playground/pages/third-parties/measuremysite-analytics.vue b/playground/pages/third-parties/measuremysite-analytics.vue new file mode 100644 index 00000000..a9c78d93 --- /dev/null +++ b/playground/pages/third-parties/measuremysite-analytics.vue @@ -0,0 +1,103 @@ + + + diff --git a/test/e2e-dev/first-party.test.ts b/test/e2e-dev/first-party.test.ts index cdfe601c..df2e0980 100644 --- a/test/e2e-dev/first-party.test.ts +++ b/test/e2e-dev/first-party.test.ts @@ -666,6 +666,7 @@ describe('first-party privacy stripping', () => { // fathomAnalytics — bundle/proxy disabled (Fathom bot-detection flags self-hosted/proxied traffic, see #720) // pulseAnalytics — bundle only, no proxy (server-side visitor identity needs the real client IP) // statableAnalytics — bundle only, no proxy (server-side visitor identity needs the real client IP) + // measuremysiteAnalytics — bundle only, no proxy (server-side visitor identity needs the real client IP) ]) /** @@ -940,6 +941,7 @@ describe('first-party privacy stripping', () => { // fathomAnalytics — bundle/proxy disabled in registry (see #720), script loads directly from CDN // pulseAnalytics — bundle only, no proxy in registry; beacons go direct to pulse-api.ciphera.net // statableAnalytics — bundle only, no proxy in registry; beacons go direct to statable.com/api/event + // measuremysiteAnalytics — bundle only, no proxy in registry; beacons go direct to a.measuremy.site/collect it('intercom', async () => { const { captures, rawCaptures, proxyRequests, externalRequests, preClickProxyCount, postClickProxyCount } = await testProvider('intercom', '/intercom-test') @@ -1005,6 +1007,7 @@ describe('first-party privacy stripping', () => { { name: 'fathomAnalytics', path: '/fathom' }, { name: 'pulseAnalytics', path: '/pulse' }, { name: 'statableAnalytics', path: '/statable' }, + { name: 'measuremysiteAnalytics', path: '/measuremysite' }, { name: 'intercom', path: '/intercom-test' }, { name: 'crisp', path: '/crisp-test' }, { name: 'posthog', path: '/posthog' }, @@ -1192,6 +1195,7 @@ describe('first-party privacy stripping', () => { { name: 'fathomAnalytics', path: '/fathom' }, { name: 'pulseAnalytics', path: '/pulse' }, { name: 'statableAnalytics', path: '/statable' }, + { name: 'measuremysiteAnalytics', path: '/measuremysite' }, { name: 'intercom', path: '/intercom-test' }, { name: 'crisp', path: '/crisp-test' }, { name: 'posthog', path: '/posthog' }, @@ -1244,6 +1248,12 @@ describe('first-party privacy stripping', () => { expect(await script.getAttribute('src')).toContain('/_scripts/assets/') } + if (name === 'measuremysiteAnalytics') { + const script = page.locator('script[data-key="pk_test123"][data-api="https://a.measuremy.site/collect"]') + expect(await script.count()).toBe(1) + expect(await script.getAttribute('src')).toContain('/_scripts/assets/') + } + // Click all buttons to trigger SDK interactions and generate proxy requests const buttons = await page.$$('button') for (const btn of buttons) { diff --git a/test/e2e/measuremysite-analytics.test.ts b/test/e2e/measuremysite-analytics.test.ts new file mode 100644 index 00000000..199120bf --- /dev/null +++ b/test/e2e/measuremysite-analytics.test.ts @@ -0,0 +1,29 @@ +import { createResolver } from '@nuxt/kit' +import { getBrowser, url } from '@nuxt/test-utils/e2e' +import { describe, expect, it } from 'vitest' +import { setupFixture } from '../utils/setup-fixture' + +const { resolve } = createResolver(import.meta.url) + +describe('measuremy.site analytics bundled script', async () => { + await setupFixture({ + rootDir: resolve('../fixtures/measuremysite-analytics'), + browser: true, + build: true, + }) + + it('keeps the site key and event endpoint on the bundled script', async () => { + const page = await (await getBrowser()).newPage() + try { + await page.goto(url('/'), { waitUntil: 'domcontentloaded' }) + await page.waitForSelector('#status:has-text("loaded")', { timeout: 10000 }) + + const script = page.locator('script[data-key="pk_test123"][data-api="https://a.measuremy.site/collect"]') + expect(await script.count()).toBe(1) + expect(await script.getAttribute('src')).toContain('/_scripts/assets/') + } + finally { + await page.close() + } + }) +}) diff --git a/test/fixtures/first-party/nuxt.config.ts b/test/fixtures/first-party/nuxt.config.ts index 1a31691d..8030d4da 100644 --- a/test/fixtures/first-party/nuxt.config.ts +++ b/test/fixtures/first-party/nuxt.config.ts @@ -26,6 +26,7 @@ export default defineNuxtConfig({ fathomAnalytics: { site: 'BRDEJWKJ' }, pulseAnalytics: { domain: 'example.com' }, statableAnalytics: { siteId: '123456' }, + measuremysiteAnalytics: { key: 'pk_test123' }, posthog: { apiKey: 'phc_CkMaDU6dr11eJoQdAiSJb1rC324dogk3T952gJ6fD9W' }, intercom: { app_id: 'akg5rmxb' }, crisp: { id: 'b1021910-7ace-425a-9ef5-07f49e5ce417' }, @@ -73,6 +74,7 @@ export default defineNuxtConfig({ fathomAnalytics: { site: 'BRDEJWKJ', trigger: 'manual' }, pulseAnalytics: { domain: 'example.com', trigger: 'manual' }, statableAnalytics: { siteId: '123456', trigger: 'manual' }, + measuremysiteAnalytics: { key: 'pk_test123', trigger: 'manual' }, posthog: { apiKey: 'phc_CkMaDU6dr11eJoQdAiSJb1rC324dogk3T952gJ6fD9W', trigger: 'manual' }, intercom: { app_id: 'akg5rmxb', trigger: 'manual' }, crisp: { id: 'b1021910-7ace-425a-9ef5-07f49e5ce417', trigger: 'manual' }, diff --git a/test/fixtures/first-party/pages/index.vue b/test/fixtures/first-party/pages/index.vue index 556e5e5c..f6e0c8ed 100644 --- a/test/fixtures/first-party/pages/index.vue +++ b/test/fixtures/first-party/pages/index.vue @@ -10,6 +10,7 @@
  • Fathom Analytics
  • Pulse Analytics
  • Statable Analytics
  • +
  • measuremy.site
  • Umami Analytics
  • PostHog
  • Rybbit Analytics
  • diff --git a/test/fixtures/first-party/pages/measuremysite.vue b/test/fixtures/first-party/pages/measuremysite.vue new file mode 100644 index 00000000..041c659e --- /dev/null +++ b/test/fixtures/first-party/pages/measuremysite.vue @@ -0,0 +1,32 @@ + + + diff --git a/test/fixtures/measuremysite-analytics/nuxt.config.ts b/test/fixtures/measuremysite-analytics/nuxt.config.ts new file mode 100644 index 00000000..94a8004d --- /dev/null +++ b/test/fixtures/measuremysite-analytics/nuxt.config.ts @@ -0,0 +1,11 @@ +import { defineNuxtConfig } from 'nuxt/config' + +export default defineNuxtConfig({ + modules: ['@nuxt/scripts'], + scripts: { + registry: { + measuremysiteAnalytics: { key: 'pk_test123', trigger: 'manual' }, + }, + }, + compatibilityDate: '2024-07-05', +}) diff --git a/test/fixtures/measuremysite-analytics/pages/index.vue b/test/fixtures/measuremysite-analytics/pages/index.vue new file mode 100644 index 00000000..974752ca --- /dev/null +++ b/test/fixtures/measuremysite-analytics/pages/index.vue @@ -0,0 +1,11 @@ + + + diff --git a/test/types/types.test-d.ts b/test/types/types.test-d.ts index 4dfb4539..f7459584 100644 --- a/test/types/types.test-d.ts +++ b/test/types/types.test-d.ts @@ -44,6 +44,7 @@ describe('module options registry', () => { expectTypeOf().not.toBeAny() expectTypeOf().not.toBeAny() expectTypeOf().not.toBeAny() + expectTypeOf().not.toBeAny() expectTypeOf().not.toBeAny() expectTypeOf().not.toBeAny() expectTypeOf().not.toBeAny() diff --git a/test/unit/measuremysite-analytics.test.ts b/test/unit/measuremysite-analytics.test.ts new file mode 100644 index 00000000..ca9159d6 --- /dev/null +++ b/test/unit/measuremysite-analytics.test.ts @@ -0,0 +1,57 @@ +/** + * @vitest-environment happy-dom + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ref } from 'vue' +import { useScriptMeasuremysiteAnalytics } from '../../packages/script/src/runtime/registry/measuremysite-analytics' + +const mocks = vi.hoisted(() => ({ + definition: undefined as any, + useRegistryScript: vi.fn(), +})) + +vi.mock('@unhead/vue', () => ({ useHead: vi.fn() })) + +vi.mock('../../packages/script/src/runtime/utils', () => ({ + useRegistryScript: mocks.useRegistryScript, +})) + +function setup(options: Record = { key: 'pk_test123' }) { + mocks.useRegistryScript.mockImplementation((_key: string, factory: (o: any) => any) => { + mocks.definition = factory(options) + return { status: ref('awaitingLoad'), signal: new AbortController().signal, load: vi.fn() } + }) + useScriptMeasuremysiteAnalytics(options as any) + return { + scriptInput: mocks.definition.scriptInput as Record, + use: mocks.definition.scriptOptions.use as () => { track: (...args: any[]) => void }, + } +} + +describe('measuremy.site analytics', () => { + beforeEach(() => { + vi.clearAllMocks() + delete (window as any).measuremysite + }) + + it('loads the tracker with the site key', () => { + const { scriptInput } = setup() + expect(scriptInput.src).toBe('https://a.measuremy.site/t.js') + expect(scriptInput['data-key']).toBe('pk_test123') + }) + + it('pins the endpoint so bundling does not change it', () => { + // The tracker posts to /collect on the origin it was served from. A bundled + // copy is served from the Nuxt origin, so data-api has to carry it instead. + const { scriptInput } = setup() + expect(scriptInput['data-api']).toBe('https://a.measuremy.site/collect') + }) + + it('exposes the tracker global once loaded', () => { + const track = vi.fn() + ;(window as any).measuremysite = { track } + const { use } = setup() + use().track('signup', { plan: 'pro' }) + expect(track).toHaveBeenCalledWith('signup', { plan: 'pro' }) + }) +}) diff --git a/test/unit/proxy-configs.test.ts b/test/unit/proxy-configs.test.ts index 8c9306e9..f5fbfd55 100644 --- a/test/unit/proxy-configs.test.ts +++ b/test/unit/proxy-configs.test.ts @@ -386,6 +386,13 @@ describe('proxy configs', () => { expect(config).toBeUndefined() }) + it('does not return proxy config for measuremysiteAnalytics (server-side visitor identity needs the real client IP)', async () => { + // measuremy.site hashes the connecting IP + user agent into its visitor identity, + // so beacons routed through the Nuxt server would merge every visitor into one. + const config = (await getProxyConfigs()).measuremysiteAnalytics + expect(config).toBeUndefined() + }) + it('returns proxy config for intercom', async () => { const config = (await getProxyConfigs()).intercom expect(config).toBeDefined() @@ -465,6 +472,7 @@ describe('proxy configs', () => { expect(configs).not.toHaveProperty('fathomAnalytics') expect(configs).not.toHaveProperty('pulseAnalytics') expect(configs).not.toHaveProperty('statableAnalytics') + expect(configs).not.toHaveProperty('measuremysiteAnalytics') expect(configs).toHaveProperty('intercom') expect(configs).not.toHaveProperty('crisp') expect(configs).not.toHaveProperty('deskcrew') From 5b66f0e32a9424f9a7ccd4c03570db3142fd910a Mon Sep 17 00:00:00 2001 From: developeron29 <> Date: Tue, 13 Oct 2026 18:50:10 +0530 Subject: [PATCH 2/3] fix: map the measuremy.site playground path by its registry label --- playground/pages/index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/pages/index.vue b/playground/pages/index.vue index 18ca41ef..c17f7e56 100644 --- a/playground/pages/index.vue +++ b/playground/pages/index.vue @@ -26,7 +26,7 @@ function getPlaygroundPath(script: any): string | null { 'rybbit-analytics': '/third-parties/rybbit-analytics', 'pulse-analytics': '/third-parties/pulse-analytics', 'statable-analytics': '/third-parties/statable-analytics', - 'measuremysite-analytics': '/third-parties/measuremysite-analytics', + 'measuremy.site': '/third-parties/measuremysite-analytics', 'databuddy-analytics': '/third-parties/databuddy-analytics', 'umami-analytics': '/third-parties/umami-analytics', 'segment': '/third-parties/segment', From d6f354a740972b9a5d196929c1ffd91f1409b225 Mon Sep 17 00:00:00 2001 From: developeron29 <> Date: Tue, 13 Oct 2026 19:08:39 +0530 Subject: [PATCH 3/3] docs: set the measuremy.site key in nuxt.config so the site can verify --- .../scripts/measuremysite-analytics.md | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/content/scripts/measuremysite-analytics.md b/docs/content/scripts/measuremysite-analytics.md index 6424df51..61ceea16 100644 --- a/docs/content/scripts/measuremysite-analytics.md +++ b/docs/content/scripts/measuremysite-analytics.md @@ -57,12 +57,38 @@ The tracker counts client-side navigation without extra setup. ## Example -The default trigger waits until Nuxt is ready: +Set the key in `nuxt.config.ts` and call the composable without options. The default trigger waits until Nuxt is ready: + +::code-group + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + scripts: { + registry: { + measuremysiteAnalytics: { key: 'pk_...' }, + }, + }, +}) +``` ```vue [app.vue] ``` + +:: + +You can also set the key with an environment variable: + +```text [.env] +NUXT_PUBLIC_SCRIPTS_MEASUREMYSITE_ANALYTICS_KEY=pk_... +``` + +## Verifying the site + +measuremy.site records nothing for a domain until it is verified. It verifies a domain by fetching its homepage and looking for the site key in the HTML. + +The tracker loads on the client, so its script tag isn't in the server-rendered HTML. A key set in `nuxt.config.ts` or `.env` is still in that HTML, because Nuxt serializes public runtime config into the page. That is enough to verify. A key passed only to `useScriptMeasuremysiteAnalytics({ key })` ends up in a JavaScript chunk instead, so the homepage check can't find it. + +If the key isn't in the homepage HTML, add the DNS TXT record from the measuremy.site Setup tab instead.