Skip to content

Commit d11be8a

Browse files
authored
feat: add Mixpanel analytics registry script (#648)
1 parent 7ef0523 commit d11be8a

File tree

10 files changed

+256
-1
lines changed

10 files changed

+256
-1
lines changed

docs/content/scripts/mixpanel.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
3+
title: Mixpanel
4+
description: Use Mixpanel in your Nuxt app.
5+
links:
6+
- label: Source
7+
icon: i-simple-icons-github
8+
to: https://github.com/nuxt/scripts/blob/main/src/runtime/registry/mixpanel-analytics.ts
9+
size: xs
10+
11+
---
12+
13+
[Mixpanel](https://mixpanel.com) is a product analytics platform that helps you understand how users interact with your application through event tracking, funnels, and retention analysis.
14+
15+
Nuxt Scripts provides a registry script composable [`useScriptMixpanelAnalytics()`{lang="ts"}](/scripts/mixpanel) to easily integrate Mixpanel in your Nuxt app.
16+
17+
::script-stats
18+
::
19+
20+
::script-docs
21+
::
22+
23+
::script-types
24+
::
25+
26+
## Examples
27+
28+
### Tracking Events
29+
30+
```vue
31+
<script setup lang="ts">
32+
const { proxy } = useScriptMixpanelAnalytics()
33+
34+
function trackSignup() {
35+
proxy.mixpanel.track('Sign Up', {
36+
plan: 'premium',
37+
source: 'landing_page',
38+
})
39+
}
40+
</script>
41+
```
42+
43+
### Identifying Users
44+
45+
```vue
46+
<script setup lang="ts">
47+
const { proxy } = useScriptMixpanelAnalytics()
48+
49+
function login(userId: string) {
50+
proxy.mixpanel.identify(userId)
51+
proxy.mixpanel.people.set({
52+
$name: 'Jane Doe',
53+
$email: 'jane@example.com',
54+
plan: 'premium',
55+
})
56+
}
57+
</script>
58+
```
59+
60+
### Registering Super Properties
61+
62+
Mixpanel sends super properties with every subsequent event:
63+
64+
```vue
65+
<script setup lang="ts">
66+
const { proxy } = useScriptMixpanelAnalytics()
67+
68+
proxy.mixpanel.register({
69+
app_version: '2.0.0',
70+
platform: 'web',
71+
})
72+
</script>
73+
```

src/module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ const REGISTRY_ENV_DEFAULTS: Partial<Record<RegistryScriptKey, Record<string, st
121121
hotjar: { id: '' },
122122
intercom: { app_id: '' },
123123
matomoAnalytics: { matomoUrl: '' },
124+
mixpanelAnalytics: { token: '' },
124125
metaPixel: { id: '' },
125126
paypal: { clientId: '' },
126127
plausibleAnalytics: { domain: '' },
@@ -143,6 +144,7 @@ const PARTYTOWN_FORWARDS: Partial<Record<RegistryScriptKey, string[]>> = {
143144
umamiAnalytics: ['umami', 'umami.track'],
144145
matomoAnalytics: ['_paq.push'],
145146
segment: ['analytics', 'analytics.track', 'analytics.page', 'analytics.identify'],
147+
mixpanelAnalytics: ['mixpanel', 'mixpanel.init', 'mixpanel.track', 'mixpanel.identify', 'mixpanel.people.set', 'mixpanel.reset', 'mixpanel.register'],
146148
metaPixel: ['fbq'],
147149
xPixel: ['twq'],
148150
tiktokPixel: ['ttq.track', 'ttq.page', 'ttq.identify'],

src/registry-logos.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const LOGOS = {
1515
dark: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 263.33 173.53" width="37.24" height="32"><g><polygon fill="#2fd381" points="181.28 171.2 227.21 123.96 261.15 171.2 181.28 171.2"/><path fill="#2fd381" d="M261.15,89.05L206.64,2.33l-33.22,17.75-34.61-7.4c2.88,5.56,4.56,12.11,4.56,19.15,0,20.03-13.46,36.26-30.06,36.26-13.66,0-25.17-11-28.83-26.06l-39.92,71.46L2.18,94.19l22.66,77.01h55.81l22.28-54.01v54.01h64.66l-49.95-82.15h143.51Z"/></g><ellipse fill="#2fd381" cx="105.94" cy="28.62" rx="12.9" ry="18.88"/></svg>`,
1616
},
1717
databuddyAnalytics: `<svg xmlns="http://www.w3.org/2000/svg" width="56.5" height="32" viewBox="0 0 8 8" shape-rendering="crispEdges"><path d="M0 0h8v8H0z"/><path fill="#fff" d="M1 1h1v6H1zm1 0h4v1H2zm4 1h1v1H6zm0 1h1v1H6zm0 1h1v1H6zm0 1h1v1H6zM2 6h4v1H2zm1-3h1v1H3zm1 1h1v1H4z"/></svg>`,
18+
mixpanelAnalytics: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="#7856FF" d="M128 0C57.308 0 0 57.308 0 128s57.308 128 128 128s128-57.308 128-128S198.692 0 128 0m-26.461 176.882c-14.478 0-26.221-11.743-26.221-26.221s11.743-26.221 26.221-26.221s26.221 11.743 26.221 26.221s-11.743 26.221-26.221 26.221m52.922 0c-14.478 0-26.221-11.743-26.221-26.221s11.743-26.221 26.221-26.221s26.221 11.743 26.221 26.221s-11.743 26.221-26.221 26.221m0-52.442c-14.478 0-26.221-11.743-26.221-26.221s11.743-26.221 26.221-26.221s26.221 11.743 26.221 26.221s-11.743 26.221-26.221 26.221"/></svg>`,
1819
segment: `<svg xmlns="http://www.w3.org/2000/svg" width="30.92" height="32" viewBox="0 0 256 265"><path fill="#4FB58B" d="m233.56 141.927l.17.013l17.892 1.87a4.927 4.927 0 0 1 3.225 1.707l.133.163l-.17.085a4.93 4.93 0 0 1 1.02 3.74a133.272 133.272 0 0 1-41.604 81.083a128.86 128.86 0 0 1-87.629 34.38a127.488 127.488 0 0 1-46.156-8.57l-.802-.312a4.716 4.716 0 0 1-2.686-2.533l-.077-.187a4.891 4.891 0 0 1-.083-3.66l7.062-17.23a4.846 4.846 0 0 1 6.118-2.799l.163.06c36.097 13.939 76.98 6.089 105.349-20.227a104.455 104.455 0 0 0 32.891-63.32a4.93 4.93 0 0 1 5.013-4.27zm-190.08 64.31l.251-.002l.253.002c8.12.093 14.658 6.659 14.746 14.749v.253c0 .084 0 .168-.002.252c-.141 8.284-6.97 14.886-15.254 14.745c-8.284-.141-14.885-6.97-14.745-15.254c.139-8.115 6.695-14.615 14.75-14.745M4.93 147.082h146.316a4.973 4.973 0 0 1 4.928 4.844l.002.171v18.316a4.974 4.974 0 0 1-4.76 5.01l-.17.005H4.93A4.975 4.975 0 0 1 0 170.584v-18.659a4.975 4.975 0 0 1 4.755-4.838zM169.56 7.311a4.974 4.974 0 0 1 2.848 2.635a5.096 5.096 0 0 1 0 3.867l-6.375 16.999a4.845 4.845 0 0 1-6.162 2.974A101.228 101.228 0 0 0 62.13 51.252a105.267 105.267 0 0 0-34.507 54.99a4.93 4.93 0 0 1-4.76 3.698h-1.105L4.25 105.733a4.886 4.886 0 0 1-3.103-2.295h-.085A4.929 4.929 0 0 1 .51 99.57a133.393 133.393 0 0 1 44.41-70.204C79.739.7 127.019-7.666 169.56 7.311m-64.807 73.434H251.07a4.972 4.972 0 0 1 4.922 4.67l.008.174v18.317a4.973 4.973 0 0 1-4.76 5.01l-.17.005H104.754a4.972 4.972 0 0 1-4.886-4.842l-.002-.173V85.759a4.972 4.972 0 0 1 4.715-5.008zm101.572-55.883l.252-.002l.253.002c8.12.093 14.658 6.659 14.746 14.748v.253c0 .085 0 .17-.002.253c-.14 8.284-6.97 14.885-15.254 14.744c-8.284-.14-14.885-6.97-14.744-15.253c.138-8.116 6.694-14.616 14.749-14.745"/></svg>`,
1920
metaPixel: `<svg xmlns="http://www.w3.org/2000/svg" width="47.91" height="32" viewBox="0 0 256 171"><defs><linearGradient id="logosMetaIcon0" x1="13.878%" x2="89.144%" y1="55.934%" y2="58.694%"><stop offset="0%" stop-color="#0064E1"/><stop offset="40%" stop-color="#0064E1"/><stop offset="83%" stop-color="#0073EE"/><stop offset="100%" stop-color="#0082FB"/></linearGradient><linearGradient id="logosMetaIcon1" x1="54.315%" x2="54.315%" y1="82.782%" y2="39.307%"><stop offset="0%" stop-color="#0082FB"/><stop offset="100%" stop-color="#0064E0"/></linearGradient></defs><path fill="#0081FB" d="M27.651 112.136c0 9.775 2.146 17.28 4.95 21.82c3.677 5.947 9.16 8.466 14.751 8.466c7.211 0 13.808-1.79 26.52-19.372c10.185-14.092 22.186-33.874 30.26-46.275l13.675-21.01c9.499-14.591 20.493-30.811 33.1-41.806C161.196 4.985 172.298 0 183.47 0c18.758 0 36.625 10.87 50.3 31.257C248.735 53.584 256 81.707 256 110.729c0 17.253-3.4 29.93-9.187 39.946c-5.591 9.686-16.488 19.363-34.818 19.363v-27.616c15.695 0 19.612-14.422 19.612-30.927c0-23.52-5.484-49.623-17.564-68.273c-8.574-13.23-19.684-21.313-31.907-21.313c-13.22 0-23.859 9.97-35.815 27.75c-6.356 9.445-12.882 20.956-20.208 33.944l-8.066 14.289c-16.203 28.728-20.307 35.271-28.408 46.07c-14.2 18.91-26.324 26.076-42.287 26.076c-18.935 0-30.91-8.2-38.325-20.556C2.973 139.413 0 126.202 0 111.148z"/><path fill="url(#logosMetaIcon0)" d="M21.802 33.206C34.48 13.666 52.774 0 73.757 0C85.91 0 97.99 3.597 110.605 13.897c13.798 11.261 28.505 29.805 46.853 60.368l6.58 10.967c15.881 26.459 24.917 40.07 30.205 46.49c6.802 8.243 11.565 10.7 17.752 10.7c15.695 0 19.612-14.422 19.612-30.927l24.393-.766c0 17.253-3.4 29.93-9.187 39.946c-5.591 9.686-16.488 19.363-34.818 19.363c-11.395 0-21.49-2.475-32.654-13.007c-8.582-8.083-18.615-22.443-26.334-35.352l-22.96-38.352C118.528 64.08 107.96 49.73 101.845 43.23c-6.578-6.988-15.036-15.428-28.532-15.428c-10.923 0-20.2 7.666-27.963 19.39z"/><path fill="url(#logosMetaIcon1)" d="M73.312 27.802c-10.923 0-20.2 7.666-27.963 19.39c-10.976 16.568-17.698 41.245-17.698 64.944c0 9.775 2.146 17.28 4.95 21.82L9.027 149.482C2.973 139.413 0 126.202 0 111.148C0 83.772 7.514 55.24 21.802 33.206C34.48 13.666 52.774 0 73.757 0z"/></svg>`,
2021
xPixel: {

src/registry-types.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,18 @@
488488
"code": "export const MetaPixelOptions = object({\n /**\n * Your Meta (Facebook) Pixel ID.\n * @see https://developers.facebook.com/docs/meta-pixel/get-started\n */\n id: union([string(), number()]),\n})"
489489
}
490490
],
491+
"mixpanel-analytics": [
492+
{
493+
"name": "MixpanelAnalyticsOptions",
494+
"kind": "const",
495+
"code": "export const MixpanelAnalyticsOptions = object({\n /**\n * Your Mixpanel project token.\n * @see https://docs.mixpanel.com/docs/tracking-methods/sdks/javascript#1-initialize-the-library\n */\n token: string(),\n})"
496+
},
497+
{
498+
"name": "MixpanelAnalyticsApi",
499+
"kind": "interface",
500+
"code": "export interface MixpanelAnalyticsApi {\n mixpanel: {\n track: (event: string, properties?: Record<string, any>) => void\n identify: (distinctId: string) => void\n reset: () => void\n people: {\n set: (properties: Record<string, any>) => void\n }\n register: (properties: Record<string, any>) => void\n init: (token: string, config?: Record<string, any>) => void\n }\n}"
501+
}
502+
],
491503
"npm": [
492504
{
493505
"name": "NpmOptions",
@@ -1713,6 +1725,14 @@
17131725
"description": "Override the analytics host URL. When first-party mode is enabled, this is auto-injected to route through the proxy. The SDK derives its API endpoint from the script src, so this changes the script src to `${analyticsHost}/script.js`."
17141726
}
17151727
],
1728+
"MixpanelAnalyticsOptions": [
1729+
{
1730+
"name": "token",
1731+
"type": "string",
1732+
"required": true,
1733+
"description": "Your Mixpanel project token."
1734+
}
1735+
],
17161736
"SegmentOptions": [
17171737
{
17181738
"name": "writeKey",

src/registry.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { GoogleAdsenseInput } from './runtime/registry/google-adsense'
44
import type { GoogleRecaptchaInput } from './runtime/registry/google-recaptcha'
55
import type { HotjarInput } from './runtime/registry/hotjar'
66
import type { IntercomInput } from './runtime/registry/intercom'
7+
import type { MixpanelAnalyticsInput } from './runtime/registry/mixpanel-analytics'
78
import type { NpmInput } from './runtime/registry/npm'
89
import type { PlausibleAnalyticsInput } from './runtime/registry/plausible-analytics'
910
import type { RybbitAnalyticsInput } from './runtime/registry/rybbit-analytics'
@@ -135,6 +136,21 @@ export async function registry(resolve?: (path: string, opts?: ResolvePathOption
135136
from: await resolve('./runtime/registry/segment'),
136137
},
137138
},
139+
{
140+
registryKey: 'mixpanelAnalytics',
141+
label: 'Mixpanel',
142+
scriptBundling: (options?: MixpanelAnalyticsInput) => {
143+
if (!options?.token)
144+
return false
145+
return 'https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js'
146+
},
147+
category: 'analytics',
148+
logo: LOGOS.mixpanelAnalytics,
149+
import: {
150+
name: 'useScriptMixpanelAnalytics',
151+
from: await resolve('./runtime/registry/mixpanel-analytics'),
152+
},
153+
},
138154
{
139155
registryKey: 'metaPixel',
140156
label: 'Meta Pixel',
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { RegistryScriptInput } from '#nuxt-scripts/types'
2+
import { useRegistryScript } from '../utils'
3+
import { MixpanelAnalyticsOptions } from './schemas'
4+
5+
export { MixpanelAnalyticsOptions }
6+
7+
export type MixpanelAnalyticsInput = RegistryScriptInput<typeof MixpanelAnalyticsOptions>
8+
9+
export interface MixpanelAnalyticsApi {
10+
mixpanel: {
11+
track: (event: string, properties?: Record<string, any>) => void
12+
identify: (distinctId: string) => void
13+
reset: () => void
14+
people: {
15+
set: (properties: Record<string, any>) => void
16+
}
17+
register: (properties: Record<string, any>) => void
18+
init: (token: string, config?: Record<string, any>) => void
19+
}
20+
}
21+
22+
declare global {
23+
interface Window {
24+
mixpanel: MixpanelAnalyticsApi['mixpanel']
25+
}
26+
}
27+
28+
const methods = ['track', 'identify', 'reset', 'register'] as const
29+
const peopleMethods = ['set'] as const
30+
31+
export function useScriptMixpanelAnalytics<T extends MixpanelAnalyticsApi>(_options?: MixpanelAnalyticsInput) {
32+
return useRegistryScript<T, typeof MixpanelAnalyticsOptions>('mixpanelAnalytics', (options) => {
33+
return {
34+
scriptInput: {
35+
src: 'https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js',
36+
},
37+
schema: import.meta.dev ? MixpanelAnalyticsOptions : undefined,
38+
scriptOptions: {
39+
use() {
40+
return {
41+
mixpanel: window.mixpanel,
42+
}
43+
},
44+
},
45+
clientInit: import.meta.server
46+
? undefined
47+
: () => {
48+
const mp = (window.mixpanel = (window.mixpanel || []) as any)
49+
if (!mp.__SV) {
50+
mp.__SV = 1.2
51+
mp._i = mp._i || []
52+
53+
mp.init = (token: string, config?: Record<string, any>, name = 'mixpanel') => {
54+
const target = name === 'mixpanel' ? mp : (mp[name] = [])
55+
target.people = target.people || []
56+
for (const method of methods) {
57+
target[method] = (...args: any[]) => {
58+
target.push([method, ...args])
59+
}
60+
}
61+
for (const method of peopleMethods) {
62+
target.people[method] = (...args: any[]) => {
63+
target.push([`people.${method}`, ...args])
64+
}
65+
}
66+
mp._i.push([token, config, name])
67+
}
68+
}
69+
70+
if (options?.token)
71+
mp.init(options.token)
72+
},
73+
}
74+
}, _options)
75+
}

src/runtime/registry/schemas.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,14 @@ export const RybbitAnalyticsOptions = object({
740740
analyticsHost: optional(string()),
741741
})
742742

743+
export const MixpanelAnalyticsOptions = object({
744+
/**
745+
* Your Mixpanel project token.
746+
* @see https://docs.mixpanel.com/docs/tracking-methods/sdks/javascript#1-initialize-the-library
747+
*/
748+
token: string(),
749+
})
750+
743751
export const SegmentOptions = object({
744752
/**
745753
* Your Segment write key.

src/runtime/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { IntercomInput } from './registry/intercom'
2424
import type { LemonSqueezyInput } from './registry/lemon-squeezy'
2525
import type { MatomoAnalyticsInput } from './registry/matomo-analytics'
2626
import type { MetaPixelInput } from './registry/meta-pixel'
27+
import type { MixpanelAnalyticsInput } from './registry/mixpanel-analytics'
2728
import type { NpmInput } from './registry/npm'
2829
import type { PayPalInput } from './registry/paypal'
2930
import type { PlausibleAnalyticsInput } from './registry/plausible-analytics'
@@ -185,6 +186,7 @@ export interface ScriptRegistry {
185186
paypal?: PayPalInput
186187
posthog?: PostHogInput
187188
matomoAnalytics?: MatomoAnalyticsInput
189+
mixpanelAnalytics?: MixpanelAnalyticsInput
188190
rybbitAnalytics?: RybbitAnalyticsInput
189191
redditPixel?: RedditPixelInput
190192
segment?: SegmentInput
@@ -212,7 +214,7 @@ export type BuiltInRegistryScriptKey
212214
| 'plausibleAnalytics' | 'googleAdsense' | 'googleAnalytics' | 'googleMaps'
213215
| 'googleRecaptcha' | 'googleSignIn' | 'lemonSqueezy' | 'googleTagManager'
214216
| 'hotjar' | 'intercom' | 'paypal' | 'posthog' | 'matomoAnalytics'
215-
| 'rybbitAnalytics' | 'redditPixel' | 'segment' | 'stripe' | 'tiktokPixel'
217+
| 'mixpanelAnalytics' | 'rybbitAnalytics' | 'redditPixel' | 'segment' | 'stripe' | 'tiktokPixel'
216218
| 'xEmbed' | 'xPixel' | 'snapchatPixel' | 'youtubePlayer' | 'vercelAnalytics'
217219
| 'vimeoPlayer' | 'umamiAnalytics' | 'gravatar' | 'npm'
218220

src/script-meta.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export const scriptMeta = {
4646
urls: ['https://cdn.databuddy.cc/databuddy.js'],
4747
trackedData: ['page-views', 'events'],
4848
},
49+
mixpanelAnalytics: {
50+
urls: ['https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js'],
51+
trackedData: ['page-views', 'events', 'conversions', 'user-identity'],
52+
},
4953
segment: {
5054
urls: ['https://cdn.segment.com/analytics.js/v1/KBXOGxgqMFjm2mxtJDJg0iDn5AnGYb9C/analytics.min.js'],
5155
trackedData: ['page-views', 'events', 'conversions', 'user-identity'],

src/script-sizes.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,60 @@
374374
}
375375
]
376376
},
377+
"mixpanelAnalytics": {
378+
"totalTransferKb": 45.2,
379+
"totalDecodedKb": 148,
380+
"loadTimeMs": 320,
381+
"collectsWebVitals": false,
382+
"apis": {
383+
"cookies": true,
384+
"localStorage": true,
385+
"sessionStorage": false,
386+
"indexedDB": false,
387+
"canvas": false,
388+
"webgl": false,
389+
"audioContext": false,
390+
"userAgent": true,
391+
"hardwareConcurrency": false,
392+
"deviceMemory": false,
393+
"plugins": false,
394+
"languages": false,
395+
"screen": true,
396+
"sendBeacon": true,
397+
"fetch": true,
398+
"xhr": true,
399+
"websocket": false,
400+
"mutationObserver": false,
401+
"performanceObserver": false,
402+
"intersectionObserver": false
403+
},
404+
"cookies": [],
405+
"network": {
406+
"requestCount": 1,
407+
"domains": [
408+
"cdn.mxpnl.com"
409+
],
410+
"outboundBytes": 0,
411+
"inboundBytes": 46285,
412+
"injectedElements": []
413+
},
414+
"performance": {
415+
"taskDurationMs": 12,
416+
"scriptDurationMs": 8,
417+
"heapDeltaKb": 1200
418+
},
419+
"scripts": [
420+
{
421+
"url": "https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js",
422+
"transferKb": 45.2,
423+
"decodedKb": 148,
424+
"encoding": "gzip",
425+
"durationMs": 320,
426+
"initiatorType": "script",
427+
"protocol": "unknown"
428+
}
429+
]
430+
},
377431
"segment": {
378432
"totalTransferKb": 29.6,
379433
"totalDecodedKb": 107,

0 commit comments

Comments
 (0)