From 82389330852cc4250290302e622b2a0c1b68389d Mon Sep 17 00:00:00 2001 From: Corey <20346601+core-e@users.noreply.github.com> Date: Thu, 23 Jul 2026 22:13:30 -0700 Subject: [PATCH 1/2] Mixpanel Web (actions): add Source Name setting Adds an optional `sourceName` setting that, when set, is registered as a Mixpanel super property named `segment_source_name`. This ports the Source Name setting from the cloud-mode Mixpanel (actions) destination. Without it, there is no straightforward way to identify in Mixpanel which Segment source initialized the SDK. `sourceName` is destructured out of settings so it is not spread into the config object passed to mixpanel.init. Opt-in and backwards compatible: when `sourceName` is blank, register is never called and behavior is unchanged. --- .../destinations/mixpanel-web/metadata.json | 10 ++ .../src/__tests__/initialization.test.ts | 91 +++++++++++++++++++ .../src/alias/__tests__/index.test.ts | 1 + .../mixpanel-web/src/generated-types.ts | 4 + .../src/group/__tests__/index.test.ts | 1 + .../src/identify/__tests__/index.test.ts | 1 + .../destinations/mixpanel-web/src/index.ts | 9 +- .../mixpanel-web/src/setting-fields.ts | 6 ++ .../src/track/__tests__/index.test.ts | 1 + .../src/trackPageView/__tests__/index.test.ts | 1 + .../destinations/mixpanel-web/src/types.ts | 2 + 11 files changed, 126 insertions(+), 1 deletion(-) diff --git a/packages/browser-destinations/destinations/mixpanel-web/metadata.json b/packages/browser-destinations/destinations/mixpanel-web/metadata.json index 47691af8ab9..0ca732927e9 100644 --- a/packages/browser-destinations/destinations/mixpanel-web/metadata.json +++ b/packages/browser-destinations/destinations/mixpanel-web/metadata.json @@ -25,6 +25,16 @@ "default": null, "depends_on": null }, + "sourceName": { + "label": "Source Name", + "description": "This value, if it's not blank, will be sent as segment_source_name to Mixpanel for every event, including events Mixpanel captures automatically.", + "type": "string", + "required": false, + "multiple": false, + "choices": null, + "default": null, + "depends_on": null + }, "api_host": { "label": "API Host", "description": "The Mixpanel API host to send data to.", diff --git a/packages/browser-destinations/destinations/mixpanel-web/src/__tests__/initialization.test.ts b/packages/browser-destinations/destinations/mixpanel-web/src/__tests__/initialization.test.ts index 692fc0b7eab..35f0a3f023a 100644 --- a/packages/browser-destinations/destinations/mixpanel-web/src/__tests__/initialization.test.ts +++ b/packages/browser-destinations/destinations/mixpanel-web/src/__tests__/initialization.test.ts @@ -51,6 +51,7 @@ describe('Mixpanel Web initialization', () => { track: jest.fn(), track_pageview: jest.fn(), identify: jest.fn(), + register: jest.fn(), alias: jest.fn(), get_group: jest.fn(), set_group: jest.fn(), @@ -99,4 +100,94 @@ describe('Mixpanel Web initialization', () => { expect(resolvedInstance).toBe(mockMixpanelInstance) expect(resolvedInstance).not.toBe(mockSnippetMixpanel) }) + + test('registers segment_source_name as a super property when sourceName is set', async () => { + const mockMixpanelInstance: Mixpanel = { + init: jest.fn(), + track: jest.fn(), + track_pageview: jest.fn(), + identify: jest.fn(), + register: jest.fn(), + alias: jest.fn(), + get_group: jest.fn(), + set_group: jest.fn(), + people: { + set: jest.fn(), + set_once: jest.fn(), + increment: jest.fn() + } + } + + const mockSnippetMixpanel: Partial = { + init: jest.fn().mockImplementation((_token, config, _name) => { + setTimeout(() => { + if (config?.loaded) { + config.loaded(mockMixpanelInstance) + } + }, 10) + }) + } + + jest.spyOn(initScriptModule, 'initScript').mockImplementation(async () => { + ;(window as any).mixpanel = mockSnippetMixpanel + }) + + const [event] = await MixpanelDestination({ + ...baseSettings, + sourceName: 'My Website Source', + subscriptions + }) + + await event.load(Context.system(), {} as Analytics) + + expect(mockMixpanelInstance.register).toHaveBeenCalledWith({ + segment_source_name: 'My Website Source' + }) + + // sourceName is a Segment side setting and is not a valid Mixpanel config option, + // so it must not be forwarded to mixpanel.init + const [, config] = (mockSnippetMixpanel.init as jest.Mock).mock.calls[0] + expect(config).not.toHaveProperty('sourceName') + }) + + test('does not register a super property when sourceName is not set', async () => { + const mockMixpanelInstance: Mixpanel = { + init: jest.fn(), + track: jest.fn(), + track_pageview: jest.fn(), + identify: jest.fn(), + register: jest.fn(), + alias: jest.fn(), + get_group: jest.fn(), + set_group: jest.fn(), + people: { + set: jest.fn(), + set_once: jest.fn(), + increment: jest.fn() + } + } + + const mockSnippetMixpanel: Partial = { + init: jest.fn().mockImplementation((_token, config, _name) => { + setTimeout(() => { + if (config?.loaded) { + config.loaded(mockMixpanelInstance) + } + }, 10) + }) + } + + jest.spyOn(initScriptModule, 'initScript').mockImplementation(async () => { + ;(window as any).mixpanel = mockSnippetMixpanel + }) + + const [event] = await MixpanelDestination({ + ...baseSettings, + subscriptions + }) + + await event.load(Context.system(), {} as Analytics) + + expect(mockMixpanelInstance.register).not.toHaveBeenCalled() + }) }) diff --git a/packages/browser-destinations/destinations/mixpanel-web/src/alias/__tests__/index.test.ts b/packages/browser-destinations/destinations/mixpanel-web/src/alias/__tests__/index.test.ts index 38003558f70..8d433604f2f 100644 --- a/packages/browser-destinations/destinations/mixpanel-web/src/alias/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/mixpanel-web/src/alias/__tests__/index.test.ts @@ -48,6 +48,7 @@ describe('Mixpanel.alias', () => { track_pageview: jest.fn(), track: jest.fn(), identify: jest.fn(), + register: jest.fn(), alias: jest.fn(), get_group: jest.fn().mockReturnValue(mockGroup), set_group: jest.fn(), diff --git a/packages/browser-destinations/destinations/mixpanel-web/src/generated-types.ts b/packages/browser-destinations/destinations/mixpanel-web/src/generated-types.ts index a07045e4d00..5401e62cb6f 100644 --- a/packages/browser-destinations/destinations/mixpanel-web/src/generated-types.ts +++ b/packages/browser-destinations/destinations/mixpanel-web/src/generated-types.ts @@ -9,6 +9,10 @@ export interface Settings { * The name for the new mixpanel instance that you want created. */ name?: string + /** + * This value, if it's not blank, will be sent as segment_source_name to Mixpanel for every event, including events Mixpanel captures automatically. + */ + sourceName?: string /** * The Mixpanel API host to send data to. */ diff --git a/packages/browser-destinations/destinations/mixpanel-web/src/group/__tests__/index.test.ts b/packages/browser-destinations/destinations/mixpanel-web/src/group/__tests__/index.test.ts index 11d266a81ec..6d4a1e2b3ec 100644 --- a/packages/browser-destinations/destinations/mixpanel-web/src/group/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/mixpanel-web/src/group/__tests__/index.test.ts @@ -48,6 +48,7 @@ describe('Mixpanel.group', () => { track_pageview: jest.fn(), track: jest.fn(), identify: jest.fn(), + register: jest.fn(), alias: jest.fn(), get_group: jest.fn().mockReturnValue(mockGroup), set_group: jest.fn(), diff --git a/packages/browser-destinations/destinations/mixpanel-web/src/identify/__tests__/index.test.ts b/packages/browser-destinations/destinations/mixpanel-web/src/identify/__tests__/index.test.ts index 16705110300..ff928a55517 100644 --- a/packages/browser-destinations/destinations/mixpanel-web/src/identify/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/mixpanel-web/src/identify/__tests__/index.test.ts @@ -48,6 +48,7 @@ describe('Mixpanel.identify', () => { track_pageview: jest.fn(), track: jest.fn(), identify: jest.fn(), + register: jest.fn(), alias: jest.fn(), get_group: jest.fn().mockReturnValue(mockGroup), set_group: jest.fn(), diff --git a/packages/browser-destinations/destinations/mixpanel-web/src/index.ts b/packages/browser-destinations/destinations/mixpanel-web/src/index.ts index 49d82e33a38..756eb11d661 100644 --- a/packages/browser-destinations/destinations/mixpanel-web/src/index.ts +++ b/packages/browser-destinations/destinations/mixpanel-web/src/index.ts @@ -29,6 +29,7 @@ export const destination: BrowserDestinationDefinition = { const { projectToken, name, + sourceName, autocapture, pageview, click, @@ -88,7 +89,13 @@ export const destination: BrowserDestinationDefinition = { } return new Promise((resolve) => { - config.loaded = (mp) => resolve(mp) + config.loaded = (mp) => { + if (sourceName) { + // Registered as a super property so it is attached to every event + mp.register({ segment_source_name: sourceName }) + } + resolve(mp) + } if (name) { window.mixpanel.init(projectToken, config, name) diff --git a/packages/browser-destinations/destinations/mixpanel-web/src/setting-fields.ts b/packages/browser-destinations/destinations/mixpanel-web/src/setting-fields.ts index eb47932e0c2..438eec8019b 100644 --- a/packages/browser-destinations/destinations/mixpanel-web/src/setting-fields.ts +++ b/packages/browser-destinations/destinations/mixpanel-web/src/setting-fields.ts @@ -13,6 +13,12 @@ export const settingFields: Record = { description: 'The name for the new mixpanel instance that you want created.', type: 'string' }, + sourceName: { + label: 'Source Name', + description: + "This value, if it's not blank, will be sent as segment_source_name to Mixpanel for every event, including events Mixpanel captures automatically.", + type: 'string' + }, api_host: { description: 'The Mixpanel API host to send data to.', label: 'API Host', diff --git a/packages/browser-destinations/destinations/mixpanel-web/src/track/__tests__/index.test.ts b/packages/browser-destinations/destinations/mixpanel-web/src/track/__tests__/index.test.ts index 1ea2e9cbb84..129260a60d1 100644 --- a/packages/browser-destinations/destinations/mixpanel-web/src/track/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/mixpanel-web/src/track/__tests__/index.test.ts @@ -48,6 +48,7 @@ describe('Mixpanel.track', () => { track_pageview: jest.fn(), track: jest.fn(), identify: jest.fn(), + register: jest.fn(), alias: jest.fn(), get_group: jest.fn().mockReturnValue(mockGroup), set_group: jest.fn(), diff --git a/packages/browser-destinations/destinations/mixpanel-web/src/trackPageView/__tests__/index.test.ts b/packages/browser-destinations/destinations/mixpanel-web/src/trackPageView/__tests__/index.test.ts index 54b53e9ea03..59f8920ad9b 100644 --- a/packages/browser-destinations/destinations/mixpanel-web/src/trackPageView/__tests__/index.test.ts +++ b/packages/browser-destinations/destinations/mixpanel-web/src/trackPageView/__tests__/index.test.ts @@ -48,6 +48,7 @@ describe('Mixpanel.trackPageView', () => { track_pageview: jest.fn(), track: jest.fn(), identify: jest.fn(), + register: jest.fn(), alias: jest.fn(), get_group: jest.fn().mockReturnValue(mockGroup), set_group: jest.fn(), diff --git a/packages/browser-destinations/destinations/mixpanel-web/src/types.ts b/packages/browser-destinations/destinations/mixpanel-web/src/types.ts index 482585810e5..8ec5ffd351b 100644 --- a/packages/browser-destinations/destinations/mixpanel-web/src/types.ts +++ b/packages/browser-destinations/destinations/mixpanel-web/src/types.ts @@ -25,6 +25,8 @@ export interface Mixpanel { identify(unique_id?: string): void + register(properties: { [k: string]: unknown }): void + alias(alias: string, original?: string): void get_group(group_key: string, group_id: string): Group From 0edeb6f31c8081b6b89db24072c8f00413741656 Mon Sep 17 00:00:00 2001 From: Corey <20346601+core-e@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:01:53 -0700 Subject: [PATCH 2/2] Mixpanel Web (actions): guard register call and trim Source Name Addresses review feedback on the Source Name change: - Trim `sourceName` before use so a whitespace-only value does not register an empty super property. - Guard `typeof mp.register === 'function'` before calling it, so an unexpected Mixpanel SDK build degrades gracefully rather than throwing a TypeError. - Add a test covering the whitespace-only case. --- .../src/__tests__/initialization.test.ts | 42 +++++++++++++++++++ .../destinations/mixpanel-web/src/index.ts | 5 ++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/browser-destinations/destinations/mixpanel-web/src/__tests__/initialization.test.ts b/packages/browser-destinations/destinations/mixpanel-web/src/__tests__/initialization.test.ts index 35f0a3f023a..6ecc8a85d5a 100644 --- a/packages/browser-destinations/destinations/mixpanel-web/src/__tests__/initialization.test.ts +++ b/packages/browser-destinations/destinations/mixpanel-web/src/__tests__/initialization.test.ts @@ -190,4 +190,46 @@ describe('Mixpanel Web initialization', () => { expect(mockMixpanelInstance.register).not.toHaveBeenCalled() }) + + test('does not register a super property when sourceName is only whitespace', async () => { + const mockMixpanelInstance: Mixpanel = { + init: jest.fn(), + track: jest.fn(), + track_pageview: jest.fn(), + identify: jest.fn(), + register: jest.fn(), + alias: jest.fn(), + get_group: jest.fn(), + set_group: jest.fn(), + people: { + set: jest.fn(), + set_once: jest.fn(), + increment: jest.fn() + } + } + + const mockSnippetMixpanel: Partial = { + init: jest.fn().mockImplementation((_token, config, _name) => { + setTimeout(() => { + if (config?.loaded) { + config.loaded(mockMixpanelInstance) + } + }, 10) + }) + } + + jest.spyOn(initScriptModule, 'initScript').mockImplementation(async () => { + ;(window as any).mixpanel = mockSnippetMixpanel + }) + + const [event] = await MixpanelDestination({ + ...baseSettings, + sourceName: ' ', + subscriptions + }) + + await event.load(Context.system(), {} as Analytics) + + expect(mockMixpanelInstance.register).not.toHaveBeenCalled() + }) }) diff --git a/packages/browser-destinations/destinations/mixpanel-web/src/index.ts b/packages/browser-destinations/destinations/mixpanel-web/src/index.ts index 756eb11d661..e43b4789dfe 100644 --- a/packages/browser-destinations/destinations/mixpanel-web/src/index.ts +++ b/packages/browser-destinations/destinations/mixpanel-web/src/index.ts @@ -90,9 +90,10 @@ export const destination: BrowserDestinationDefinition = { return new Promise((resolve) => { config.loaded = (mp) => { - if (sourceName) { + const trimmedSourceName = sourceName?.trim() + if (trimmedSourceName && typeof mp?.register === 'function') { // Registered as a super property so it is attached to every event - mp.register({ segment_source_name: sourceName }) + mp.register({ segment_source_name: trimmedSourceName }) } resolve(mp) }