@ably/ui 18.1.0 → 18.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/core/Code.js.map +1 -1
- package/core/Flash.js.map +1 -1
- package/core/Loader.js.map +1 -1
- package/core/Notice.js.map +1 -1
- package/core/SessionData.js.map +1 -1
- package/core/hooks/use-themed-scrollpoints.test.js +1 -1
- package/core/hooks/use-themed-scrollpoints.test.js.map +1 -1
- package/core/insights/command-queue.js +1 -1
- package/core/insights/command-queue.js.map +1 -1
- package/core/insights/index.js +1 -1
- package/core/insights/index.js.map +1 -1
- package/core/insights/index.test.js +1 -1
- package/core/insights/index.test.js.map +1 -1
- package/core/insights/mixpanel.js +1 -1
- package/core/insights/mixpanel.js.map +1 -1
- package/core/insights/mixpanel.test.js +1 -1
- package/core/insights/mixpanel.test.js.map +1 -1
- package/core/insights/posthog.js +1 -1
- package/core/insights/posthog.js.map +1 -1
- package/core/insights/posthog.test.js +1 -1
- package/core/insights/posthog.test.js.map +1 -1
- package/core/insights/service.js +1 -1
- package/core/insights/service.js.map +1 -1
- package/core/insights/types.js.map +1 -1
- package/core/utils/sanitize-html.js +2 -0
- package/core/utils/sanitize-html.js.map +1 -0
- package/core/utils/sanitize-html.test.js +2 -0
- package/core/utils/sanitize-html.test.js.map +1 -0
- package/index.d.ts +48 -68
- package/package.json +12 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/core/insights/index.test.ts"],"sourcesContent":["/**\n * @vitest-environment jsdom\n */\n\nimport { describe, expect, beforeEach, afterEach, it, vi, Mock } from \"vitest\";\n\nimport * as datalayer from \"./datalayer\";\nimport * as mixpanel from \"./mixpanel\";\nimport * as posthog from \"./posthog\";\nimport * as logger from \"./logger\";\nimport * as insights from \"./index\";\n\n// Mock the dependencies\nvi.mock(\"./datalayer\", () => ({\n track: vi.fn(),\n trackPageView: vi.fn(),\n}));\n\nvi.mock(\"./mixpanel\", () => ({\n initMixpanel: vi.fn(),\n enableDebugMode: vi.fn(),\n disableDebugMode: vi.fn(),\n identify: vi.fn(),\n trackPageView: vi.fn(),\n track: vi.fn(),\n startSessionRecording: vi.fn(),\n stopSessionRecording: vi.fn(),\n}));\n\nvi.mock(\"./posthog\", () => ({\n initPosthog: vi.fn(),\n enableDebugMode: vi.fn(),\n disableDebugMode: vi.fn(),\n identify: vi.fn(),\n trackPageView: vi.fn(),\n track: vi.fn(),\n startSessionRecording: vi.fn(),\n stopSessionRecording: vi.fn(),\n}));\n\nvi.mock(\"./logger\", () => ({\n debug: vi.fn(),\n info: vi.fn(),\n warn: vi.fn(),\n error: vi.fn(),\n}));\n\ndescribe(\"Insights Command Queue\", () => {\n const testConfig = {\n debug: true,\n mixpanelToken: \"test-token\",\n mixpanelAutoCapture: false,\n mixpanelRecordSessionsPercent: 10,\n posthogApiKey: \"test-key\",\n posthogApiHost: \"test-host\",\n };\n\n const testIdentity = {\n userId: \"user-123\",\n accountId: \"account-456\",\n organisationId: \"org-789\",\n email: \"test@example.com\",\n name: \"Test User\",\n };\n\n beforeEach(() => {\n // Clear all mocks before each test\n vi.clearAllMocks();\n\n // Reset the module to clear any internal state\n vi.resetModules();\n });\n\n afterEach(() => {\n // Cleanup document event listeners\n document.body.replaceWith(document.body.cloneNode(true));\n });\n\n describe(\"Pre-initialization Queueing\", () => {\n it(\"should queue methods called before initialization\", async () => {\n // Call methods before initialization\n insights.track(\"early_event\", { early: true });\n insights.identify(testIdentity);\n insights.trackPageView();\n\n // Verify nothing has been called yet on the underlying services\n expect(mixpanel.track).not.toHaveBeenCalled();\n expect(posthog.track).not.toHaveBeenCalled();\n expect(datalayer.track).not.toHaveBeenCalled();\n expect(mixpanel.identify).not.toHaveBeenCalled();\n expect(posthog.identify).not.toHaveBeenCalled();\n expect(mixpanel.trackPageView).not.toHaveBeenCalled();\n expect(posthog.trackPageView).not.toHaveBeenCalled();\n expect(datalayer.trackPageView).not.toHaveBeenCalled();\n\n // Now initialize\n insights.initInsights(testConfig);\n\n // Initialize should be called immediately\n expect(mixpanel.initMixpanel).toHaveBeenCalledWith(\n testConfig.mixpanelToken,\n testConfig.mixpanelAutoCapture,\n testConfig.debug,\n testConfig.mixpanelRecordSessionsPercent,\n );\n expect(posthog.initPosthog).toHaveBeenCalledWith(\n testConfig.posthogApiKey,\n testConfig.posthogApiHost,\n );\n\n // Queued methods should now be called in the correct order\n expect(mixpanel.track).toHaveBeenCalledWith(\"early_event\", {\n early: true,\n });\n expect(posthog.track).toHaveBeenCalledWith(\"early_event\", {\n early: true,\n });\n expect(datalayer.track).toHaveBeenCalledWith(\"early_event\", {\n early: true,\n });\n\n expect(mixpanel.identify).toHaveBeenCalledWith(testIdentity);\n expect(posthog.identify).toHaveBeenCalledWith(testIdentity);\n\n expect(mixpanel.trackPageView).toHaveBeenCalled();\n expect(posthog.trackPageView).toHaveBeenCalled();\n expect(datalayer.trackPageView).not.toHaveBeenCalled();\n });\n\n it(\"should handle errors in queued methods gracefully\", async () => {\n // Setup an error for one of the methods\n (mixpanel.track as Mock).mockImplementationOnce(() => {\n throw new Error(\"Mixpanel error\");\n });\n\n // Call methods before initialization\n insights.track(\"error_event\", { error: true });\n insights.trackPageView();\n\n // Now initialize\n insights.initInsights(testConfig);\n\n // Should have logged the error but continued processing the queue\n expect(logger.error).toHaveBeenCalledWith(\n expect.stringContaining(\"Failed to track event in Mixpanel\"),\n expect.any(Error),\n );\n\n // The other methods should still be called\n expect(posthog.track).toHaveBeenCalledWith(\"error_event\", {\n error: true,\n });\n expect(datalayer.track).toHaveBeenCalledWith(\"error_event\", {\n error: true,\n });\n expect(mixpanel.trackPageView).toHaveBeenCalled();\n expect(posthog.trackPageView).toHaveBeenCalled();\n expect(datalayer.trackPageView).not.toHaveBeenCalled();\n });\n\n it(\"should report page view to GTM as well when includeDataLayer is true\", () => {\n insights.trackPageView({ includeDataLayer: true });\n expect(mixpanel.trackPageView).toHaveBeenCalled();\n expect(posthog.trackPageView).toHaveBeenCalled();\n expect(datalayer.trackPageView).toHaveBeenCalled();\n });\n });\n\n describe(\"Post-initialization Direct Execution\", () => {\n beforeEach(() => {\n // Initialize first\n insights.initInsights(testConfig);\n // Clear the mocks to focus on post-init behavior\n vi.clearAllMocks();\n });\n\n it(\"should directly call methods after initialization\", () => {\n // Call methods after initialization\n insights.track(\"post_init_event\", { post: true });\n\n // Should be called immediately\n expect(mixpanel.track).toHaveBeenCalledWith(\"post_init_event\", {\n post: true,\n });\n expect(posthog.track).toHaveBeenCalledWith(\"post_init_event\", {\n post: true,\n });\n expect(datalayer.track).toHaveBeenCalledWith(\"post_init_event\", {\n post: true,\n });\n });\n\n it(\"should handle all exported methods correctly\", () => {\n // Test each exported method\n insights.identify(testIdentity);\n expect(mixpanel.identify).toHaveBeenCalledWith(testIdentity);\n expect(posthog.identify).toHaveBeenCalledWith(testIdentity);\n\n insights.trackPageView();\n expect(mixpanel.trackPageView).toHaveBeenCalled();\n expect(posthog.trackPageView).toHaveBeenCalled();\n expect(datalayer.trackPageView).not.toHaveBeenCalled();\n\n insights.startSessionRecording();\n expect(mixpanel.startSessionRecording).toHaveBeenCalled();\n expect(posthog.startSessionRecording).toHaveBeenCalled();\n\n insights.stopSessionRecording();\n expect(mixpanel.stopSessionRecording).toHaveBeenCalled();\n expect(posthog.stopSessionRecording).toHaveBeenCalled();\n\n insights.enableDebugMode();\n expect(mixpanel.enableDebugMode).toHaveBeenCalled();\n expect(posthog.enableDebugMode).toHaveBeenCalled();\n\n insights.disableDebugMode();\n expect(mixpanel.disableDebugMode).toHaveBeenCalled();\n expect(posthog.disableDebugMode).toHaveBeenCalled();\n });\n\n it(\"should report page view to GTM as well when includeDataLayer is true\", () => {\n insights.trackPageView({ includeDataLayer: true });\n expect(mixpanel.trackPageView).toHaveBeenCalled();\n expect(posthog.trackPageView).toHaveBeenCalled();\n expect(datalayer.trackPageView).toHaveBeenCalled();\n });\n });\n\n describe(\"Observer Setup\", () => {\n beforeEach(() => {\n insights.initInsights(testConfig);\n vi.clearAllMocks();\n });\n\n it(\"should set up click event observer and track clicks\", () => {\n // Setup observer\n const cleanup = insights.setupObserver();\n\n // Create a test element with insight attributes\n const testElement = document.createElement(\"button\");\n testElement.setAttribute(\"data-insight-event\", \"button_clicked\");\n testElement.setAttribute(\"data-insight-button-id\", \"test-123\");\n document.body.appendChild(testElement);\n\n // Simulate click\n testElement.click();\n\n // Should track the event\n expect(mixpanel.track).toHaveBeenCalledWith(\"button_clicked\", {\n buttonId: \"test-123\",\n });\n expect(posthog.track).toHaveBeenCalledWith(\"button_clicked\", {\n buttonId: \"test-123\",\n });\n expect(datalayer.track).toHaveBeenCalledWith(\"button_clicked\", {\n buttonId: \"test-123\",\n });\n\n // Test cleanup\n cleanup();\n\n // Reset tracking mocks\n vi.clearAllMocks();\n\n // Click again - should not track\n testElement.click();\n expect(mixpanel.track).not.toHaveBeenCalled();\n });\n\n it(\"should handle nested elements correctly\", () => {\n // Setup observer\n insights.setupObserver();\n\n // Create parent element with insight attributes\n const parentElement = document.createElement(\"div\");\n parentElement.setAttribute(\"data-insight-event\", \"container_clicked\");\n parentElement.setAttribute(\n \"data-insight-container-id\",\n \"parent-container\",\n );\n\n // Create child element without insights\n const childElement = document.createElement(\"span\");\n childElement.textContent = \"Click me\";\n\n // Nest elements\n parentElement.appendChild(childElement);\n document.body.appendChild(parentElement);\n\n // Click the child element\n childElement.click();\n\n // Should find and use the parent's insight attributes\n expect(mixpanel.track).toHaveBeenCalledWith(\"container_clicked\", {\n containerId: \"parent-container\",\n });\n });\n });\n\n describe(\"Error Handling\", () => {\n it(\"should handle initialization errors gracefully\", () => {\n // Setup an error in initialization\n (mixpanel.initMixpanel as Mock).mockImplementationOnce(() => {\n throw new Error(\"Mixpanel init error\");\n });\n\n // Should not throw when initializing\n expect(() => {\n insights.initInsights(testConfig);\n }).not.toThrow();\n\n // Should log the error\n expect(logger.error).toHaveBeenCalledWith(\n expect.stringContaining(\"Failed to initialize Mixpanel\"),\n expect.any(Error),\n );\n });\n\n it(\"should handle runtime errors in methods\", () => {\n // Initialize first\n insights.initInsights(testConfig);\n vi.clearAllMocks();\n\n // Setup errors in tracking\n (mixpanel.track as Mock).mockImplementationOnce(() => {\n throw new Error(\"Mixpanel track error\");\n });\n\n (posthog.track as Mock).mockImplementationOnce(() => {\n throw new Error(\"Posthog track error\");\n });\n\n // Should not throw when tracking\n expect(() => {\n insights.track(\"error_test\", { test: true });\n }).not.toThrow();\n\n // Should log the errors\n expect(logger.error).toHaveBeenCalledWith(\n expect.stringContaining(\"Failed to track event in Mixpanel\"),\n expect.any(Error),\n );\n\n expect(logger.error).toHaveBeenCalledWith(\n expect.stringContaining(\"Failed to track event in Posthog\"),\n expect.any(Error),\n );\n\n // Should still try to track with datalayer\n expect(datalayer.track).toHaveBeenCalledWith(\"error_test\", {\n test: true,\n });\n });\n });\n\n describe(\"Debug Mode\", () => {\n it(\"should respect debug flag in config\", () => {\n // Initialize with debug: true\n insights.initInsights(testConfig);\n\n // Should log debug messages\n expect(logger.debug).toHaveBeenCalledWith(\n expect.stringContaining(\"Initializing insights\"),\n );\n\n // Clear mocks and test tracking\n vi.clearAllMocks();\n insights.track(\"debug_test\", { debug: true });\n\n // Should log info about tracking\n expect(logger.info).toHaveBeenCalledWith(\n expect.stringContaining(\"Tracking event\"),\n expect.objectContaining({\n event: \"debug_test\",\n properties: { debug: true },\n }),\n );\n });\n\n it(\"should not log debug info when debug is false\", () => {\n // Initialize with debug: false\n insights.initInsights({\n ...testConfig,\n debug: false,\n });\n\n // Clear mocks and test tracking\n vi.clearAllMocks();\n insights.track(\"no_debug_test\", { debug: false });\n\n // Should not log info about tracking\n expect(logger.info).not.toHaveBeenCalled();\n });\n });\n\n describe(\"Arbitrary Properties\", () => {\n const customPageViewProperties = {\n customProperty: \"custom-value\",\n anotherProperty: 123,\n nestedObject: { foo: \"bar\" },\n };\n\n const customPageViewPropertiesWithDataLayer = {\n customProperty: \"custom-value\",\n anotherProperty: 456,\n };\n\n const customPageViewPropertiesWithExcludeIds = {\n customProperty: \"test\",\n count: 789,\n };\n\n const customIdentityProperties = {\n customProperty: \"user-custom-value\",\n userSegment: \"premium\",\n signupDate: \"2024-01-01\",\n };\n\n const customMinimalIdentityProperties = {\n customField1: \"value1\",\n customField2: 999,\n customField3: { nested: true },\n };\n\n beforeEach(() => {\n insights.initInsights(testConfig);\n vi.clearAllMocks();\n });\n\n it(\"should pass arbitrary properties through trackPageView\", () => {\n // Call trackPageView with custom properties\n insights.trackPageView(customPageViewProperties);\n\n // Verify mixpanel received all properties\n expect(mixpanel.trackPageView).toHaveBeenCalledWith(\n customPageViewProperties,\n );\n\n // Verify posthog received all properties\n expect(posthog.trackPageView).toHaveBeenCalledWith(\n customPageViewProperties,\n );\n\n // Verify datalayer did not receive anything (includeDataLayer not set)\n expect(datalayer.trackPageView).not.toHaveBeenCalled();\n });\n\n it(\"should pass arbitrary properties through trackPageView with includeDataLayer\", () => {\n // Call trackPageView with custom properties and includeDataLayer\n insights.trackPageView({\n includeDataLayer: true,\n ...customPageViewPropertiesWithDataLayer,\n });\n\n // Verify mixpanel received custom properties (includeDataLayer filtered out)\n expect(mixpanel.trackPageView).toHaveBeenCalledWith(\n customPageViewPropertiesWithDataLayer,\n );\n\n // Verify posthog received custom properties (includeDataLayer filtered out)\n expect(posthog.trackPageView).toHaveBeenCalledWith(\n customPageViewPropertiesWithDataLayer,\n );\n\n // Verify datalayer received custom properties (includeDataLayer filtered out)\n expect(datalayer.trackPageView).toHaveBeenCalledWith(\n customPageViewPropertiesWithDataLayer,\n );\n });\n\n it(\"should pass arbitrary properties through trackPageView with excludeIds\", () => {\n const excludeIds = [\"id1\", \"id2\"];\n\n // Call trackPageView with custom properties and excludeIds\n insights.trackPageView({\n excludeIds,\n ...customPageViewPropertiesWithExcludeIds,\n });\n\n // Verify mixpanel received excludeIds and custom properties\n expect(mixpanel.trackPageView).toHaveBeenCalledWith({\n excludeIds,\n ...customPageViewPropertiesWithExcludeIds,\n });\n\n // Verify posthog received only custom properties (excludeIds filtered out)\n expect(posthog.trackPageView).toHaveBeenCalledWith(\n customPageViewPropertiesWithExcludeIds,\n );\n });\n\n it(\"should pass arbitrary properties through identify\", () => {\n const identityWithCustomProps = {\n ...testIdentity,\n ...customIdentityProperties,\n };\n\n // Call identify with custom properties\n insights.identify(identityWithCustomProps);\n\n // Verify mixpanel received all properties\n expect(mixpanel.identify).toHaveBeenCalledWith(identityWithCustomProps);\n\n // Verify posthog received all properties\n expect(posthog.identify).toHaveBeenCalledWith(identityWithCustomProps);\n });\n\n it(\"should pass arbitrary properties through identify with minimal identity\", () => {\n const minimalIdentity = {\n userId: \"minimal-user\",\n accountId: \"minimal-account\",\n ...customMinimalIdentityProperties,\n };\n\n // Call identify with only required fields plus custom properties\n insights.identify(minimalIdentity);\n\n // Verify mixpanel received all properties\n expect(mixpanel.identify).toHaveBeenCalledWith(minimalIdentity);\n\n // Verify posthog received all properties\n expect(posthog.identify).toHaveBeenCalledWith(minimalIdentity);\n });\n });\n});\n"],"names":["describe","expect","beforeEach","afterEach","it","vi","datalayer","mixpanel","posthog","logger","insights","mock","track","fn","trackPageView","initMixpanel","enableDebugMode","disableDebugMode","identify","startSessionRecording","stopSessionRecording","initPosthog","debug","info","warn","error","testConfig","mixpanelToken","mixpanelAutoCapture","mixpanelRecordSessionsPercent","posthogApiKey","posthogApiHost","testIdentity","userId","accountId","organisationId","email","name","clearAllMocks","resetModules","document","body","replaceWith","cloneNode","early","not","toHaveBeenCalled","initInsights","toHaveBeenCalledWith","mockImplementationOnce","Error","stringContaining","any","includeDataLayer","post","cleanup","setupObserver","testElement","createElement","setAttribute","appendChild","click","buttonId","parentElement","childElement","textContent","containerId","toThrow","test","objectContaining","event","properties","customPageViewProperties","customProperty","anotherProperty","nestedObject","foo","customPageViewPropertiesWithDataLayer","customPageViewPropertiesWithExcludeIds","count","customIdentityProperties","userSegment","signupDate","customMinimalIdentityProperties","customField1","customField2","customField3","nested","excludeIds","identityWithCustomProps","minimalIdentity"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/core/insights/index.test.ts"],"sourcesContent":["/**\n * @vitest-environment jsdom\n */\n\nimport { describe, expect, beforeEach, afterEach, it, vi, Mock } from \"vitest\";\n\nimport * as datalayer from \"./datalayer\";\nimport * as mixpanel from \"./mixpanel\";\nimport * as posthog from \"./posthog\";\nimport * as logger from \"./logger\";\nimport * as insights from \"./index\";\n\n// Mock the dependencies\nvi.mock(\"./datalayer\", () => ({\n track: vi.fn(),\n trackPageView: vi.fn(),\n}));\n\nvi.mock(\"./mixpanel\", () => ({\n initMixpanel: vi.fn(),\n enableDebugMode: vi.fn(),\n disableDebugMode: vi.fn(),\n identify: vi.fn(),\n trackPageView: vi.fn(),\n track: vi.fn(),\n}));\n\nvi.mock(\"./posthog\", () => ({\n initPosthog: vi.fn(),\n enableDebugMode: vi.fn(),\n disableDebugMode: vi.fn(),\n identify: vi.fn(),\n trackPageView: vi.fn(),\n track: vi.fn(),\n}));\n\nvi.mock(\"./logger\", () => ({\n debug: vi.fn(),\n info: vi.fn(),\n warn: vi.fn(),\n error: vi.fn(),\n}));\n\ndescribe(\"Insights Command Queue\", () => {\n const testConfig = {\n debug: true,\n mixpanelToken: \"test-token\",\n mixpanelAutoCapture: false,\n posthogApiKey: \"test-key\",\n posthogApiHost: \"test-host\",\n };\n\n const testIdentity = {\n userId: \"user-123\",\n accountId: \"account-456\",\n organisationId: \"org-789\",\n email: \"test@example.com\",\n name: \"Test User\",\n };\n\n beforeEach(() => {\n // Clear all mocks before each test\n vi.clearAllMocks();\n\n // Reset the module to clear any internal state\n vi.resetModules();\n });\n\n afterEach(() => {\n // Cleanup document event listeners\n document.body.replaceWith(document.body.cloneNode(true));\n });\n\n describe(\"Pre-initialization Queueing\", () => {\n it(\"should queue methods called before initialization\", async () => {\n // Call methods before initialization\n insights.track(\"early_event\", { early: true });\n insights.identify(testIdentity);\n insights.trackPageView();\n\n // Verify nothing has been called yet on the underlying services\n expect(mixpanel.track).not.toHaveBeenCalled();\n expect(posthog.track).not.toHaveBeenCalled();\n expect(datalayer.track).not.toHaveBeenCalled();\n expect(mixpanel.identify).not.toHaveBeenCalled();\n expect(posthog.identify).not.toHaveBeenCalled();\n expect(mixpanel.trackPageView).not.toHaveBeenCalled();\n expect(posthog.trackPageView).not.toHaveBeenCalled();\n expect(datalayer.trackPageView).not.toHaveBeenCalled();\n\n // Now initialize\n insights.initInsights(testConfig);\n\n // Initialize should be called immediately\n expect(mixpanel.initMixpanel).toHaveBeenCalledWith(\n testConfig.mixpanelToken,\n testConfig.mixpanelAutoCapture,\n testConfig.debug,\n undefined,\n );\n expect(posthog.initPosthog).toHaveBeenCalledWith(\n testConfig.posthogApiKey,\n testConfig.posthogApiHost,\n );\n\n // Queued methods should now be called in the correct order\n expect(mixpanel.track).toHaveBeenCalledWith(\"early_event\", {\n early: true,\n });\n expect(posthog.track).toHaveBeenCalledWith(\"early_event\", {\n early: true,\n });\n expect(datalayer.track).toHaveBeenCalledWith(\"early_event\", {\n early: true,\n });\n\n expect(mixpanel.identify).toHaveBeenCalledWith(testIdentity);\n expect(posthog.identify).toHaveBeenCalledWith(testIdentity);\n\n expect(mixpanel.trackPageView).toHaveBeenCalled();\n expect(posthog.trackPageView).toHaveBeenCalled();\n expect(datalayer.trackPageView).not.toHaveBeenCalled();\n });\n\n it(\"should handle errors in queued methods gracefully\", async () => {\n // Setup an error for one of the methods\n (mixpanel.track as Mock).mockImplementationOnce(() => {\n throw new Error(\"Mixpanel error\");\n });\n\n // Call methods before initialization\n insights.track(\"error_event\", { error: true });\n insights.trackPageView();\n\n // Now initialize\n insights.initInsights(testConfig);\n\n // Should have logged the error but continued processing the queue\n expect(logger.error).toHaveBeenCalledWith(\n expect.stringContaining(\"Failed to track event in Mixpanel\"),\n expect.any(Error),\n );\n\n // The other methods should still be called\n expect(posthog.track).toHaveBeenCalledWith(\"error_event\", {\n error: true,\n });\n expect(datalayer.track).toHaveBeenCalledWith(\"error_event\", {\n error: true,\n });\n expect(mixpanel.trackPageView).toHaveBeenCalled();\n expect(posthog.trackPageView).toHaveBeenCalled();\n expect(datalayer.trackPageView).not.toHaveBeenCalled();\n });\n\n it(\"should report page view to GTM as well when includeDataLayer is true\", () => {\n insights.trackPageView({ includeDataLayer: true });\n expect(mixpanel.trackPageView).toHaveBeenCalled();\n expect(posthog.trackPageView).toHaveBeenCalled();\n expect(datalayer.trackPageView).toHaveBeenCalled();\n });\n });\n\n describe(\"Post-initialization Direct Execution\", () => {\n beforeEach(() => {\n // Initialize first\n insights.initInsights(testConfig);\n // Clear the mocks to focus on post-init behavior\n vi.clearAllMocks();\n });\n\n it(\"should directly call methods after initialization\", () => {\n // Call methods after initialization\n insights.track(\"post_init_event\", { post: true });\n\n // Should be called immediately\n expect(mixpanel.track).toHaveBeenCalledWith(\"post_init_event\", {\n post: true,\n });\n expect(posthog.track).toHaveBeenCalledWith(\"post_init_event\", {\n post: true,\n });\n expect(datalayer.track).toHaveBeenCalledWith(\"post_init_event\", {\n post: true,\n });\n });\n\n it(\"should handle all exported methods correctly\", () => {\n // Test each exported method\n insights.identify(testIdentity);\n expect(mixpanel.identify).toHaveBeenCalledWith(testIdentity);\n expect(posthog.identify).toHaveBeenCalledWith(testIdentity);\n\n insights.trackPageView();\n expect(mixpanel.trackPageView).toHaveBeenCalled();\n expect(posthog.trackPageView).toHaveBeenCalled();\n expect(datalayer.trackPageView).not.toHaveBeenCalled();\n\n insights.enableDebugMode();\n expect(mixpanel.enableDebugMode).toHaveBeenCalled();\n expect(posthog.enableDebugMode).toHaveBeenCalled();\n\n insights.disableDebugMode();\n expect(mixpanel.disableDebugMode).toHaveBeenCalled();\n expect(posthog.disableDebugMode).toHaveBeenCalled();\n });\n\n it(\"should report page view to GTM as well when includeDataLayer is true\", () => {\n insights.trackPageView({ includeDataLayer: true });\n expect(mixpanel.trackPageView).toHaveBeenCalled();\n expect(posthog.trackPageView).toHaveBeenCalled();\n expect(datalayer.trackPageView).toHaveBeenCalled();\n });\n });\n\n describe(\"Observer Setup\", () => {\n beforeEach(() => {\n insights.initInsights(testConfig);\n vi.clearAllMocks();\n });\n\n it(\"should set up click event observer and track clicks\", () => {\n // Setup observer\n const cleanup = insights.setupObserver();\n\n // Create a test element with insight attributes\n const testElement = document.createElement(\"button\");\n testElement.setAttribute(\"data-insight-event\", \"button_clicked\");\n testElement.setAttribute(\"data-insight-button-id\", \"test-123\");\n document.body.appendChild(testElement);\n\n // Simulate click\n testElement.click();\n\n // Should track the event\n expect(mixpanel.track).toHaveBeenCalledWith(\"button_clicked\", {\n buttonId: \"test-123\",\n });\n expect(posthog.track).toHaveBeenCalledWith(\"button_clicked\", {\n buttonId: \"test-123\",\n });\n expect(datalayer.track).toHaveBeenCalledWith(\"button_clicked\", {\n buttonId: \"test-123\",\n });\n\n // Test cleanup\n cleanup();\n\n // Reset tracking mocks\n vi.clearAllMocks();\n\n // Click again - should not track\n testElement.click();\n expect(mixpanel.track).not.toHaveBeenCalled();\n });\n\n it(\"should handle nested elements correctly\", () => {\n // Setup observer\n insights.setupObserver();\n\n // Create parent element with insight attributes\n const parentElement = document.createElement(\"div\");\n parentElement.setAttribute(\"data-insight-event\", \"container_clicked\");\n parentElement.setAttribute(\n \"data-insight-container-id\",\n \"parent-container\",\n );\n\n // Create child element without insights\n const childElement = document.createElement(\"span\");\n childElement.textContent = \"Click me\";\n\n // Nest elements\n parentElement.appendChild(childElement);\n document.body.appendChild(parentElement);\n\n // Click the child element\n childElement.click();\n\n // Should find and use the parent's insight attributes\n expect(mixpanel.track).toHaveBeenCalledWith(\"container_clicked\", {\n containerId: \"parent-container\",\n });\n });\n });\n\n describe(\"Error Handling\", () => {\n it(\"should handle initialization errors gracefully\", () => {\n // Setup an error in initialization\n (mixpanel.initMixpanel as Mock).mockImplementationOnce(() => {\n throw new Error(\"Mixpanel init error\");\n });\n\n // Should not throw when initializing\n expect(() => {\n insights.initInsights(testConfig);\n }).not.toThrow();\n\n // Should log the error\n expect(logger.error).toHaveBeenCalledWith(\n expect.stringContaining(\"Failed to initialize Mixpanel\"),\n expect.any(Error),\n );\n });\n\n it(\"should handle runtime errors in methods\", () => {\n // Initialize first\n insights.initInsights(testConfig);\n vi.clearAllMocks();\n\n // Setup errors in tracking\n (mixpanel.track as Mock).mockImplementationOnce(() => {\n throw new Error(\"Mixpanel track error\");\n });\n\n (posthog.track as Mock).mockImplementationOnce(() => {\n throw new Error(\"Posthog track error\");\n });\n\n // Should not throw when tracking\n expect(() => {\n insights.track(\"error_test\", { test: true });\n }).not.toThrow();\n\n // Should log the errors\n expect(logger.error).toHaveBeenCalledWith(\n expect.stringContaining(\"Failed to track event in Mixpanel\"),\n expect.any(Error),\n );\n\n expect(logger.error).toHaveBeenCalledWith(\n expect.stringContaining(\"Failed to track event in Posthog\"),\n expect.any(Error),\n );\n\n // Should still try to track with datalayer\n expect(datalayer.track).toHaveBeenCalledWith(\"error_test\", {\n test: true,\n });\n });\n });\n\n describe(\"Debug Mode\", () => {\n it(\"should respect debug flag in config\", () => {\n // Initialize with debug: true\n insights.initInsights(testConfig);\n\n // Should log debug messages\n expect(logger.debug).toHaveBeenCalledWith(\n expect.stringContaining(\"Initializing insights\"),\n );\n\n // Clear mocks and test tracking\n vi.clearAllMocks();\n insights.track(\"debug_test\", { debug: true });\n\n // Should log info about tracking\n expect(logger.info).toHaveBeenCalledWith(\n expect.stringContaining(\"Tracking event\"),\n expect.objectContaining({\n event: \"debug_test\",\n properties: { debug: true },\n }),\n );\n });\n\n it(\"should not log debug info when debug is false\", () => {\n // Initialize with debug: false\n insights.initInsights({\n ...testConfig,\n debug: false,\n });\n\n // Clear mocks and test tracking\n vi.clearAllMocks();\n insights.track(\"no_debug_test\", { debug: false });\n\n // Should not log info about tracking\n expect(logger.info).not.toHaveBeenCalled();\n });\n });\n\n describe(\"Arbitrary Properties\", () => {\n const customPageViewProperties = {\n customProperty: \"custom-value\",\n anotherProperty: 123,\n nestedObject: { foo: \"bar\" },\n };\n\n const customPageViewPropertiesWithDataLayer = {\n customProperty: \"custom-value\",\n anotherProperty: 456,\n };\n\n const customPageViewPropertiesWithExcludeIds = {\n customProperty: \"test\",\n count: 789,\n };\n\n const customIdentityProperties = {\n customProperty: \"user-custom-value\",\n userSegment: \"premium\",\n signupDate: \"2024-01-01\",\n };\n\n const customMinimalIdentityProperties = {\n customField1: \"value1\",\n customField2: 999,\n customField3: { nested: true },\n };\n\n beforeEach(() => {\n insights.initInsights(testConfig);\n vi.clearAllMocks();\n });\n\n it(\"should pass arbitrary properties through trackPageView\", () => {\n // Call trackPageView with custom properties\n insights.trackPageView(customPageViewProperties);\n\n // Verify mixpanel received all properties\n expect(mixpanel.trackPageView).toHaveBeenCalledWith(\n customPageViewProperties,\n );\n\n // Verify posthog received all properties\n expect(posthog.trackPageView).toHaveBeenCalledWith(\n customPageViewProperties,\n );\n\n // Verify datalayer did not receive anything (includeDataLayer not set)\n expect(datalayer.trackPageView).not.toHaveBeenCalled();\n });\n\n it(\"should pass arbitrary properties through trackPageView with includeDataLayer\", () => {\n // Call trackPageView with custom properties and includeDataLayer\n insights.trackPageView({\n includeDataLayer: true,\n ...customPageViewPropertiesWithDataLayer,\n });\n\n // Verify mixpanel received custom properties (includeDataLayer filtered out)\n expect(mixpanel.trackPageView).toHaveBeenCalledWith(\n customPageViewPropertiesWithDataLayer,\n );\n\n // Verify posthog received custom properties (includeDataLayer filtered out)\n expect(posthog.trackPageView).toHaveBeenCalledWith(\n customPageViewPropertiesWithDataLayer,\n );\n\n // Verify datalayer received custom properties (includeDataLayer filtered out)\n expect(datalayer.trackPageView).toHaveBeenCalledWith(\n customPageViewPropertiesWithDataLayer,\n );\n });\n\n it(\"should pass arbitrary properties through trackPageView with excludeIds\", () => {\n const excludeIds = [\"id1\", \"id2\"];\n\n // Call trackPageView with custom properties and excludeIds\n insights.trackPageView({\n excludeIds,\n ...customPageViewPropertiesWithExcludeIds,\n });\n\n // Verify mixpanel received excludeIds and custom properties\n expect(mixpanel.trackPageView).toHaveBeenCalledWith({\n excludeIds,\n ...customPageViewPropertiesWithExcludeIds,\n });\n\n // Verify posthog received only custom properties (excludeIds filtered out)\n expect(posthog.trackPageView).toHaveBeenCalledWith(\n customPageViewPropertiesWithExcludeIds,\n );\n });\n\n it(\"should pass arbitrary properties through identify\", () => {\n const identityWithCustomProps = {\n ...testIdentity,\n ...customIdentityProperties,\n };\n\n // Call identify with custom properties\n insights.identify(identityWithCustomProps);\n\n // Verify mixpanel received all properties\n expect(mixpanel.identify).toHaveBeenCalledWith(identityWithCustomProps);\n\n // Verify posthog received all properties\n expect(posthog.identify).toHaveBeenCalledWith(identityWithCustomProps);\n });\n\n it(\"should pass arbitrary properties through identify with minimal identity\", () => {\n const minimalIdentity = {\n userId: \"minimal-user\",\n accountId: \"minimal-account\",\n ...customMinimalIdentityProperties,\n };\n\n // Call identify with only required fields plus custom properties\n insights.identify(minimalIdentity);\n\n // Verify mixpanel received all properties\n expect(mixpanel.identify).toHaveBeenCalledWith(minimalIdentity);\n\n // Verify posthog received all properties\n expect(posthog.identify).toHaveBeenCalledWith(minimalIdentity);\n });\n });\n});\n"],"names":["describe","expect","beforeEach","afterEach","it","vi","datalayer","mixpanel","posthog","logger","insights","mock","track","fn","trackPageView","initMixpanel","enableDebugMode","disableDebugMode","identify","initPosthog","debug","info","warn","error","testConfig","mixpanelToken","mixpanelAutoCapture","posthogApiKey","posthogApiHost","testIdentity","userId","accountId","organisationId","email","name","clearAllMocks","resetModules","document","body","replaceWith","cloneNode","early","not","toHaveBeenCalled","initInsights","toHaveBeenCalledWith","undefined","mockImplementationOnce","Error","stringContaining","any","includeDataLayer","post","cleanup","setupObserver","testElement","createElement","setAttribute","appendChild","click","buttonId","parentElement","childElement","textContent","containerId","toThrow","test","objectContaining","event","properties","customPageViewProperties","customProperty","anotherProperty","nestedObject","foo","customPageViewPropertiesWithDataLayer","customPageViewPropertiesWithExcludeIds","count","customIdentityProperties","userSegment","signupDate","customMinimalIdentityProperties","customField1","customField2","customField3","nested","excludeIds","identityWithCustomProps","minimalIdentity"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import mixpanel from"mixpanel-browser";export const initMixpanel=(token,autoCapture=false,debug=false,
|
|
1
|
+
import mixpanel from"mixpanel-browser";export const MIXPANEL_EU_API_HOST="https://api-eu.mixpanel.com";export const initMixpanel=(token,autoCapture=false,debug=false,apiHost=MIXPANEL_EU_API_HOST)=>{const blockSelectors=["[data-mask]"];if(!token){console.warn("Mixpanel token not provided, skipping initialization");return}mixpanel.init(token,{api_host:apiHost,debug:debug,persistence:"localStorage",autocapture:autoCapture?{block_selectors:blockSelectors,capture_text_content:true,pageview:false}:false,track_pageview:false,record_sessions_percent:0})};export const enableDebugMode=()=>{mixpanel.set_config({debug:true})};export const disableDebugMode=()=>{mixpanel.set_config({debug:false})};export const identify=({userId,accountId,organisationId,email,name,...properties})=>{if(!userId){return}mixpanel.identify(userId.toString());const peopleProperties={...properties};if(email||name){peopleProperties.$email=email;peopleProperties.$name=name}if(organisationId){peopleProperties.organization_id=[organisationId.toString()]}if(Object.keys(peopleProperties).length>0){mixpanel.people.set(peopleProperties)}if(accountId){mixpanel.people.union({accounts:[accountId.toString()]})}};const redactUrlSegments=excludeIds=>{const pathSegments=window.location.pathname.split("/");const redactedSegments=pathSegments.map(segment=>{if(/^\d+$/.test(segment)||excludeIds.some(id=>id&&id!==""&&segment===id)){return"{redacted}"}return segment});const url=new URL(window.location.href);url.pathname=redactedSegments.join("/");return decodeURI(url.toString()).toLowerCase()};export const trackPageView=properties=>{const{excludeIds,...rest}=properties??{};mixpanel.track_pageview({redacted_path:redactUrlSegments(excludeIds??[]),...rest})};export const track=(event,properties)=>{mixpanel.track(event,properties)};
|
|
2
2
|
//# sourceMappingURL=mixpanel.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/core/insights/mixpanel.ts"],"sourcesContent":["import mixpanel from \"mixpanel-browser\";\n\nimport { InsightsIdentity, TrackPageViewOptions } from \"./types\";\n\nexport const initMixpanel = (\n token: string,\n autoCapture: boolean = false,\n debug: boolean = false,\n
|
|
1
|
+
{"version":3,"sources":["../../../src/core/insights/mixpanel.ts"],"sourcesContent":["import mixpanel from \"mixpanel-browser\";\n\nimport { InsightsIdentity, TrackPageViewOptions } from \"./types\";\n\nexport const MIXPANEL_EU_API_HOST = \"https://api-eu.mixpanel.com\";\n\nexport const initMixpanel = (\n token: string,\n autoCapture: boolean = false,\n debug: boolean = false,\n apiHost: string = MIXPANEL_EU_API_HOST,\n) => {\n const blockSelectors = [\"[data-mask]\"];\n if (!token) {\n console.warn(\"Mixpanel token not provided, skipping initialization\");\n return;\n }\n\n mixpanel.init(token, {\n api_host: apiHost,\n debug: debug,\n persistence: \"localStorage\",\n autocapture: autoCapture\n ? {\n block_selectors: blockSelectors,\n capture_text_content: true,\n pageview: false, // We'll track page views manually\n }\n : false,\n track_pageview: false, // We'll track page views manually\n record_sessions_percent: 0,\n });\n};\n\nexport const enableDebugMode = () => {\n mixpanel.set_config({ debug: true });\n};\n\nexport const disableDebugMode = () => {\n mixpanel.set_config({ debug: false });\n};\n\nexport const identify = ({\n userId,\n accountId,\n organisationId,\n email,\n name,\n ...properties\n}: InsightsIdentity) => {\n // In very rare cases we might have a user without an account, so we'll\n // let null/undefined/blank strings through on that one\n if (!userId) {\n return;\n }\n\n mixpanel.identify(userId.toString());\n\n const peopleProperties: Record<string, unknown> = { ...properties };\n\n if (email || name) {\n peopleProperties.$email = email;\n peopleProperties.$name = name;\n }\n\n if (organisationId) {\n peopleProperties.organization_id = [organisationId.toString()];\n }\n\n if (Object.keys(peopleProperties).length > 0) {\n mixpanel.people.set(peopleProperties);\n }\n\n if (accountId) {\n mixpanel.people.union({ accounts: [accountId.toString()] });\n }\n};\n\n// Simple function to replace all digits and IDs in a URL path with {redacted},\n// purely to make reporting based on aggregates easier\nconst redactUrlSegments = (excludeIds: string[]) => {\n const pathSegments = window.location.pathname.split(\"/\");\n\n const redactedSegments = pathSegments.map((segment) => {\n // Redact if the segment contains only digits or matches any of the excluded IDs\n if (\n /^\\d+$/.test(segment) ||\n excludeIds.some((id) => id && id !== \"\" && segment === id)\n ) {\n return \"{redacted}\";\n }\n\n return segment;\n });\n\n // Join the segments back together\n const url = new URL(window.location.href);\n url.pathname = redactedSegments.join(\"/\");\n\n return decodeURI(url.toString()).toLowerCase();\n};\n\nexport const trackPageView = (properties?: TrackPageViewOptions) => {\n const { excludeIds, ...rest } = properties ?? {};\n\n // Add the redacted URL to the page view event for reporting\n mixpanel.track_pageview({\n redacted_path: redactUrlSegments(excludeIds ?? []),\n ...rest,\n });\n};\n\nexport const track = (event: string, properties?: Record<string, unknown>) => {\n mixpanel.track(event, properties);\n};\n"],"names":["mixpanel","MIXPANEL_EU_API_HOST","initMixpanel","token","autoCapture","debug","apiHost","blockSelectors","console","warn","init","api_host","persistence","autocapture","block_selectors","capture_text_content","pageview","track_pageview","record_sessions_percent","enableDebugMode","set_config","disableDebugMode","identify","userId","accountId","organisationId","email","name","properties","toString","peopleProperties","$email","$name","organization_id","Object","keys","length","people","set","union","accounts","redactUrlSegments","excludeIds","pathSegments","window","location","pathname","split","redactedSegments","map","segment","test","some","id","url","URL","href","join","decodeURI","toLowerCase","trackPageView","rest","redacted_path","track","event"],"mappings":"AAAA,OAAOA,aAAc,kBAAmB,AAIxC,QAAO,MAAMC,qBAAuB,6BAA8B,AAElE,QAAO,MAAMC,aAAe,CAC1BC,MACAC,YAAuB,KAAK,CAC5BC,MAAiB,KAAK,CACtBC,QAAkBL,oBAAoB,IAEtC,MAAMM,eAAiB,CAAC,cAAc,CACtC,GAAI,CAACJ,MAAO,CACVK,QAAQC,IAAI,CAAC,wDACb,MACF,CAEAT,SAASU,IAAI,CAACP,MAAO,CACnBQ,SAAUL,QACVD,MAAOA,MACPO,YAAa,eACbC,YAAaT,YACT,CACEU,gBAAiBP,eACjBQ,qBAAsB,KACtBC,SAAU,KACZ,EACA,MACJC,eAAgB,MAChBC,wBAAyB,CAC3B,EACF,CAAE,AAEF,QAAO,MAAMC,gBAAkB,KAC7BnB,SAASoB,UAAU,CAAC,CAAEf,MAAO,IAAK,EACpC,CAAE,AAEF,QAAO,MAAMgB,iBAAmB,KAC9BrB,SAASoB,UAAU,CAAC,CAAEf,MAAO,KAAM,EACrC,CAAE,AAEF,QAAO,MAAMiB,SAAW,CAAC,CACvBC,MAAM,CACNC,SAAS,CACTC,cAAc,CACdC,KAAK,CACLC,IAAI,CACJ,GAAGC,WACc,IAGjB,GAAI,CAACL,OAAQ,CACX,MACF,CAEAvB,SAASsB,QAAQ,CAACC,OAAOM,QAAQ,IAEjC,MAAMC,iBAA4C,CAAE,GAAGF,UAAU,AAAC,EAElE,GAAIF,OAASC,KAAM,CACjBG,iBAAiBC,MAAM,CAAGL,KAC1BI,CAAAA,iBAAiBE,KAAK,CAAGL,IAC3B,CAEA,GAAIF,eAAgB,CAClBK,iBAAiBG,eAAe,CAAG,CAACR,eAAeI,QAAQ,GAAG,AAChE,CAEA,GAAIK,OAAOC,IAAI,CAACL,kBAAkBM,MAAM,CAAG,EAAG,CAC5CpC,SAASqC,MAAM,CAACC,GAAG,CAACR,iBACtB,CAEA,GAAIN,UAAW,CACbxB,SAASqC,MAAM,CAACE,KAAK,CAAC,CAAEC,SAAU,CAAChB,UAAUK,QAAQ,GAAG,AAAC,EAC3D,CACF,CAAE,CAIF,MAAMY,kBAAoB,AAACC,aACzB,MAAMC,aAAeC,OAAOC,QAAQ,CAACC,QAAQ,CAACC,KAAK,CAAC,KAEpD,MAAMC,iBAAmBL,aAAaM,GAAG,CAAC,AAACC,UAEzC,GACE,QAAQC,IAAI,CAACD,UACbR,WAAWU,IAAI,CAAC,AAACC,IAAOA,IAAMA,KAAO,IAAMH,UAAYG,IACvD,CACA,MAAO,YACT,CAEA,OAAOH,OACT,GAGA,MAAMI,IAAM,IAAIC,IAAIX,OAAOC,QAAQ,CAACW,IAAI,CACxCF,CAAAA,IAAIR,QAAQ,CAAGE,iBAAiBS,IAAI,CAAC,KAErC,OAAOC,UAAUJ,IAAIzB,QAAQ,IAAI8B,WAAW,EAC9C,CAEA,QAAO,MAAMC,cAAgB,AAAChC,aAC5B,KAAM,CAAEc,UAAU,CAAE,GAAGmB,KAAM,CAAGjC,YAAc,CAAC,EAG/C5B,SAASiB,cAAc,CAAC,CACtB6C,cAAerB,kBAAkBC,YAAc,EAAE,EACjD,GAAGmB,IAAI,AACT,EACF,CAAE,AAEF,QAAO,MAAME,MAAQ,CAACC,MAAepC,cACnC5B,SAAS+D,KAAK,CAACC,MAAOpC,WACxB,CAAE"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{describe,expect,beforeEach,it,vi}from"vitest";import mixpanel from"mixpanel-browser";import{initMixpanel}from"./mixpanel";vi.mock("mixpanel-browser",()=>({default:{init:vi.fn()}}));describe("initMixpanel",()=>{beforeEach(()=>{vi.clearAllMocks()});it("does not initialize without a token",()=>{const consoleSpy=vi.spyOn(console,"warn").mockImplementation(()=>{});initMixpanel("");expect(mixpanel.init).not.toHaveBeenCalled();consoleSpy.mockRestore()});it("blocks [data-mask] elements in autocapture",()=>{initMixpanel("test-token",true);const config=mixpanel.init.mock.calls[0][1];expect(config.autocapture.block_selectors).toEqual(["[data-mask]"])});it("
|
|
1
|
+
import{describe,expect,beforeEach,it,vi}from"vitest";import mixpanel from"mixpanel-browser";import{initMixpanel,MIXPANEL_EU_API_HOST}from"./mixpanel";vi.mock("mixpanel-browser",()=>({default:{init:vi.fn()}}));describe("initMixpanel",()=>{beforeEach(()=>{vi.clearAllMocks()});it("does not initialize without a token",()=>{const consoleSpy=vi.spyOn(console,"warn").mockImplementation(()=>{});initMixpanel("");expect(mixpanel.init).not.toHaveBeenCalled();consoleSpy.mockRestore()});it("blocks [data-mask] elements in autocapture",()=>{initMixpanel("test-token",true);const config=mixpanel.init.mock.calls[0][1];expect(config.autocapture.block_selectors).toEqual(["[data-mask]"])});it("does not record sessions",()=>{initMixpanel("test-token",true);const config=mixpanel.init.mock.calls[0][1];expect(config.record_sessions_percent).toBe(0)});it("defaults api_host to the EU ingestion endpoint",()=>{initMixpanel("test-token");const config=mixpanel.init.mock.calls[0][1];expect(config.api_host).toBe(MIXPANEL_EU_API_HOST);expect(MIXPANEL_EU_API_HOST).toBe("https://api-eu.mixpanel.com")});it("allows overriding api_host",()=>{initMixpanel("test-token",false,false,"https://api.mixpanel.com");const config=mixpanel.init.mock.calls[0][1];expect(config.api_host).toBe("https://api.mixpanel.com")});it("falls back to the EU default when api_host is undefined",()=>{initMixpanel("test-token",false,false,undefined);const config=mixpanel.init.mock.calls[0][1];expect(config.api_host).toBe(MIXPANEL_EU_API_HOST)})});
|
|
2
2
|
//# sourceMappingURL=mixpanel.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/core/insights/mixpanel.test.ts"],"sourcesContent":["/**\n * @vitest-environment jsdom\n */\n\nimport { describe, expect, beforeEach, it, vi } from \"vitest\";\nimport mixpanel from \"mixpanel-browser\";\n\nimport { initMixpanel } from \"./mixpanel\";\n\nvi.mock(\"mixpanel-browser\", () => ({\n default: {\n init: vi.fn(),\n },\n}));\n\ndescribe(\"initMixpanel\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n it(\"does not initialize without a token\", () => {\n const consoleSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n initMixpanel(\"\");\n expect(mixpanel.init).not.toHaveBeenCalled();\n consoleSpy.mockRestore();\n });\n\n it(\"blocks [data-mask] elements in autocapture\", () => {\n initMixpanel(\"test-token\", true);\n\n const config = (mixpanel.init as ReturnType<typeof vi.fn>).mock.calls[0][1];\n expect(config.autocapture.block_selectors).toEqual([\"[data-mask]\"]);\n });\n\n it(\"
|
|
1
|
+
{"version":3,"sources":["../../../src/core/insights/mixpanel.test.ts"],"sourcesContent":["/**\n * @vitest-environment jsdom\n */\n\nimport { describe, expect, beforeEach, it, vi } from \"vitest\";\nimport mixpanel from \"mixpanel-browser\";\n\nimport { initMixpanel, MIXPANEL_EU_API_HOST } from \"./mixpanel\";\n\nvi.mock(\"mixpanel-browser\", () => ({\n default: {\n init: vi.fn(),\n },\n}));\n\ndescribe(\"initMixpanel\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n it(\"does not initialize without a token\", () => {\n const consoleSpy = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n initMixpanel(\"\");\n expect(mixpanel.init).not.toHaveBeenCalled();\n consoleSpy.mockRestore();\n });\n\n it(\"blocks [data-mask] elements in autocapture\", () => {\n initMixpanel(\"test-token\", true);\n\n const config = (mixpanel.init as ReturnType<typeof vi.fn>).mock.calls[0][1];\n expect(config.autocapture.block_selectors).toEqual([\"[data-mask]\"]);\n });\n\n it(\"does not record sessions\", () => {\n initMixpanel(\"test-token\", true);\n\n const config = (mixpanel.init as ReturnType<typeof vi.fn>).mock.calls[0][1];\n expect(config.record_sessions_percent).toBe(0);\n });\n\n it(\"defaults api_host to the EU ingestion endpoint\", () => {\n initMixpanel(\"test-token\");\n\n const config = (mixpanel.init as ReturnType<typeof vi.fn>).mock.calls[0][1];\n expect(config.api_host).toBe(MIXPANEL_EU_API_HOST);\n expect(MIXPANEL_EU_API_HOST).toBe(\"https://api-eu.mixpanel.com\");\n });\n\n it(\"allows overriding api_host\", () => {\n initMixpanel(\"test-token\", false, false, \"https://api.mixpanel.com\");\n\n const config = (mixpanel.init as ReturnType<typeof vi.fn>).mock.calls[0][1];\n expect(config.api_host).toBe(\"https://api.mixpanel.com\");\n });\n\n it(\"falls back to the EU default when api_host is undefined\", () => {\n initMixpanel(\"test-token\", false, false, undefined);\n\n const config = (mixpanel.init as ReturnType<typeof vi.fn>).mock.calls[0][1];\n expect(config.api_host).toBe(MIXPANEL_EU_API_HOST);\n });\n});\n"],"names":["describe","expect","beforeEach","it","vi","mixpanel","initMixpanel","MIXPANEL_EU_API_HOST","mock","default","init","fn","clearAllMocks","consoleSpy","spyOn","console","mockImplementation","not","toHaveBeenCalled","mockRestore","config","calls","autocapture","block_selectors","toEqual","record_sessions_percent","toBe","api_host","undefined"],"mappings":"AAIA,OAASA,QAAQ,CAAEC,MAAM,CAAEC,UAAU,CAAEC,EAAE,CAAEC,EAAE,KAAQ,QAAS,AAC9D,QAAOC,aAAc,kBAAmB,AAExC,QAASC,YAAY,CAAEC,oBAAoB,KAAQ,YAAa,CAEhEH,GAAGI,IAAI,CAAC,mBAAoB,IAAO,CAAA,CACjCC,QAAS,CACPC,KAAMN,GAAGO,EAAE,EACb,CACF,CAAA,GAEAX,SAAS,eAAgB,KACvBE,WAAW,KACTE,GAAGQ,aAAa,EAClB,GAEAT,GAAG,sCAAuC,KACxC,MAAMU,WAAaT,GAAGU,KAAK,CAACC,QAAS,QAAQC,kBAAkB,CAAC,KAAO,GACvEV,aAAa,IACbL,OAAOI,SAASK,IAAI,EAAEO,GAAG,CAACC,gBAAgB,GAC1CL,WAAWM,WAAW,EACxB,GAEAhB,GAAG,6CAA8C,KAC/CG,aAAa,aAAc,MAE3B,MAAMc,OAAS,AAACf,SAASK,IAAI,CAA8BF,IAAI,CAACa,KAAK,CAAC,EAAE,CAAC,EAAE,CAC3EpB,OAAOmB,OAAOE,WAAW,CAACC,eAAe,EAAEC,OAAO,CAAC,CAAC,cAAc,CACpE,GAEArB,GAAG,2BAA4B,KAC7BG,aAAa,aAAc,MAE3B,MAAMc,OAAS,AAACf,SAASK,IAAI,CAA8BF,IAAI,CAACa,KAAK,CAAC,EAAE,CAAC,EAAE,CAC3EpB,OAAOmB,OAAOK,uBAAuB,EAAEC,IAAI,CAAC,EAC9C,GAEAvB,GAAG,iDAAkD,KACnDG,aAAa,cAEb,MAAMc,OAAS,AAACf,SAASK,IAAI,CAA8BF,IAAI,CAACa,KAAK,CAAC,EAAE,CAAC,EAAE,CAC3EpB,OAAOmB,OAAOO,QAAQ,EAAED,IAAI,CAACnB,sBAC7BN,OAAOM,sBAAsBmB,IAAI,CAAC,8BACpC,GAEAvB,GAAG,6BAA8B,KAC/BG,aAAa,aAAc,MAAO,MAAO,4BAEzC,MAAMc,OAAS,AAACf,SAASK,IAAI,CAA8BF,IAAI,CAACa,KAAK,CAAC,EAAE,CAAC,EAAE,CAC3EpB,OAAOmB,OAAOO,QAAQ,EAAED,IAAI,CAAC,2BAC/B,GAEAvB,GAAG,0DAA2D,KAC5DG,aAAa,aAAc,MAAO,MAAOsB,WAEzC,MAAMR,OAAS,AAACf,SAASK,IAAI,CAA8BF,IAAI,CAACa,KAAK,CAAC,EAAE,CAAC,EAAE,CAC3EpB,OAAOmB,OAAOO,QAAQ,EAAED,IAAI,CAACnB,qBAC/B,EACF"}
|
package/core/insights/posthog.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import posthog from"posthog-js";export const initPosthog=(apiKey,apiHost)=>{if(!apiKey){return}posthog.init(apiKey,{api_host:apiHost,capture_pageview:false,
|
|
1
|
+
import posthog from"posthog-js";export const initPosthog=(apiKey,apiHost)=>{if(!apiKey){return}posthog.init(apiKey,{api_host:apiHost,capture_pageview:false,disable_session_recording:true})};export const enableDebugMode=()=>{posthog.debug()};export const disableDebugMode=()=>{posthog.debug(false)};export const identify=({userId,accountId,organisationId,email,name,...properties})=>{if(!userId){return}if(userId!==posthog.get_distinct_id()){posthog.identify(userId,{email,name,...properties})}if(accountId){posthog.group("account",accountId)}if(organisationId){posthog.group("organisation",organisationId)}};export const trackPageView=properties=>{posthog.capture("$pageview",properties)};export const track=(event,properties)=>{posthog.capture(event,properties)};
|
|
2
2
|
//# sourceMappingURL=posthog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/core/insights/posthog.ts"],"sourcesContent":["import posthog from \"posthog-js\";\n\nimport { InsightsIdentity } from \"./types\";\n\nexport const initPosthog = (apiKey: string, apiHost: string) => {\n if (!apiKey) {\n return;\n }\n\n posthog.init(apiKey, {\n api_host: apiHost,\n capture_pageview: false,\n
|
|
1
|
+
{"version":3,"sources":["../../../src/core/insights/posthog.ts"],"sourcesContent":["import posthog from \"posthog-js\";\n\nimport { InsightsIdentity } from \"./types\";\n\nexport const initPosthog = (apiKey: string, apiHost: string) => {\n if (!apiKey) {\n return;\n }\n\n posthog.init(apiKey, {\n api_host: apiHost,\n capture_pageview: false,\n disable_session_recording: true,\n });\n};\n\nexport const enableDebugMode = () => {\n posthog.debug();\n};\n\nexport const disableDebugMode = () => {\n posthog.debug(false);\n};\n\nexport const identify = ({\n userId,\n accountId,\n organisationId,\n email,\n name,\n ...properties\n}: InsightsIdentity) => {\n // In very rare cases we might have a user without an account, so we'll\n // let null/undefined/blank strings through on that one\n if (!userId) {\n return;\n }\n\n if (userId !== posthog.get_distinct_id()) {\n posthog.identify(userId, { email, name, ...properties });\n }\n\n // Associate all events in this session with this account\n if (accountId) {\n posthog.group(\"account\", accountId);\n }\n\n // Associate all events in this session with this organisation (if available)\n if (organisationId) {\n posthog.group(\"organisation\", organisationId);\n }\n};\n\nexport const trackPageView = (properties?: Record<string, unknown>) => {\n posthog.capture(\"$pageview\", properties);\n};\n\nexport const track = (event: string, properties?: Record<string, unknown>) => {\n posthog.capture(event, properties);\n};\n"],"names":["posthog","initPosthog","apiKey","apiHost","init","api_host","capture_pageview","disable_session_recording","enableDebugMode","debug","disableDebugMode","identify","userId","accountId","organisationId","email","name","properties","get_distinct_id","group","trackPageView","capture","track","event"],"mappings":"AAAA,OAAOA,YAAa,YAAa,AAIjC,QAAO,MAAMC,YAAc,CAACC,OAAgBC,WAC1C,GAAI,CAACD,OAAQ,CACX,MACF,CAEAF,QAAQI,IAAI,CAACF,OAAQ,CACnBG,SAAUF,QACVG,iBAAkB,MAClBC,0BAA2B,IAC7B,EACF,CAAE,AAEF,QAAO,MAAMC,gBAAkB,KAC7BR,QAAQS,KAAK,EACf,CAAE,AAEF,QAAO,MAAMC,iBAAmB,KAC9BV,QAAQS,KAAK,CAAC,MAChB,CAAE,AAEF,QAAO,MAAME,SAAW,CAAC,CACvBC,MAAM,CACNC,SAAS,CACTC,cAAc,CACdC,KAAK,CACLC,IAAI,CACJ,GAAGC,WACc,IAGjB,GAAI,CAACL,OAAQ,CACX,MACF,CAEA,GAAIA,SAAWZ,QAAQkB,eAAe,GAAI,CACxClB,QAAQW,QAAQ,CAACC,OAAQ,CAAEG,MAAOC,KAAM,GAAGC,UAAU,AAAC,EACxD,CAGA,GAAIJ,UAAW,CACbb,QAAQmB,KAAK,CAAC,UAAWN,UAC3B,CAGA,GAAIC,eAAgB,CAClBd,QAAQmB,KAAK,CAAC,eAAgBL,eAChC,CACF,CAAE,AAEF,QAAO,MAAMM,cAAgB,AAACH,aAC5BjB,QAAQqB,OAAO,CAAC,YAAaJ,WAC/B,CAAE,AAEF,QAAO,MAAMK,MAAQ,CAACC,MAAeN,cACnCjB,QAAQqB,OAAO,CAACE,MAAON,WACzB,CAAE"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{describe,expect,beforeEach,it,vi}from"vitest";import posthog from"posthog-js";import{initPosthog}from"./posthog";vi.mock("posthog-js",()=>({default:{init:vi.fn()}}));describe("initPosthog",()=>{beforeEach(()=>{vi.clearAllMocks()});it("does not initialize without an API key",()=>{initPosthog("","https://app.posthog.com");expect(posthog.init).not.toHaveBeenCalled()});it("initializes with session recording
|
|
1
|
+
import{describe,expect,beforeEach,it,vi}from"vitest";import posthog from"posthog-js";import{initPosthog}from"./posthog";vi.mock("posthog-js",()=>({default:{init:vi.fn()}}));describe("initPosthog",()=>{beforeEach(()=>{vi.clearAllMocks()});it("does not initialize without an API key",()=>{initPosthog("","https://app.posthog.com");expect(posthog.init).not.toHaveBeenCalled()});it("initializes with session recording disabled",()=>{initPosthog("phc_test","https://app.posthog.com");expect(posthog.init).toHaveBeenCalledWith("phc_test",{api_host:"https://app.posthog.com",capture_pageview:false,disable_session_recording:true})})});
|
|
2
2
|
//# sourceMappingURL=posthog.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/core/insights/posthog.test.ts"],"sourcesContent":["/**\n * @vitest-environment jsdom\n */\n\nimport { describe, expect, beforeEach, it, vi } from \"vitest\";\nimport posthog from \"posthog-js\";\n\nimport { initPosthog } from \"./posthog\";\n\nvi.mock(\"posthog-js\", () => ({\n default: {\n init: vi.fn(),\n },\n}));\n\ndescribe(\"initPosthog\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n it(\"does not initialize without an API key\", () => {\n initPosthog(\"\", \"https://app.posthog.com\");\n expect(posthog.init).not.toHaveBeenCalled();\n });\n\n it(\"initializes with session recording
|
|
1
|
+
{"version":3,"sources":["../../../src/core/insights/posthog.test.ts"],"sourcesContent":["/**\n * @vitest-environment jsdom\n */\n\nimport { describe, expect, beforeEach, it, vi } from \"vitest\";\nimport posthog from \"posthog-js\";\n\nimport { initPosthog } from \"./posthog\";\n\nvi.mock(\"posthog-js\", () => ({\n default: {\n init: vi.fn(),\n },\n}));\n\ndescribe(\"initPosthog\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n it(\"does not initialize without an API key\", () => {\n initPosthog(\"\", \"https://app.posthog.com\");\n expect(posthog.init).not.toHaveBeenCalled();\n });\n\n it(\"initializes with session recording disabled\", () => {\n initPosthog(\"phc_test\", \"https://app.posthog.com\");\n\n expect(posthog.init).toHaveBeenCalledWith(\"phc_test\", {\n api_host: \"https://app.posthog.com\",\n capture_pageview: false,\n disable_session_recording: true,\n });\n });\n});\n"],"names":["describe","expect","beforeEach","it","vi","posthog","initPosthog","mock","default","init","fn","clearAllMocks","not","toHaveBeenCalled","toHaveBeenCalledWith","api_host","capture_pageview","disable_session_recording"],"mappings":"AAIA,OAASA,QAAQ,CAAEC,MAAM,CAAEC,UAAU,CAAEC,EAAE,CAAEC,EAAE,KAAQ,QAAS,AAC9D,QAAOC,YAAa,YAAa,AAEjC,QAASC,WAAW,KAAQ,WAAY,CAExCF,GAAGG,IAAI,CAAC,aAAc,IAAO,CAAA,CAC3BC,QAAS,CACPC,KAAML,GAAGM,EAAE,EACb,CACF,CAAA,GAEAV,SAAS,cAAe,KACtBE,WAAW,KACTE,GAAGO,aAAa,EAClB,GAEAR,GAAG,yCAA0C,KAC3CG,YAAY,GAAI,2BAChBL,OAAOI,QAAQI,IAAI,EAAEG,GAAG,CAACC,gBAAgB,EAC3C,GAEAV,GAAG,8CAA+C,KAChDG,YAAY,WAAY,2BAExBL,OAAOI,QAAQI,IAAI,EAAEK,oBAAoB,CAAC,WAAY,CACpDC,SAAU,0BACVC,iBAAkB,MAClBC,0BAA2B,IAC7B,EACF,EACF"}
|
package/core/insights/service.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function _define_property(obj,key,value){if(key in obj){Object.defineProperty(obj,key,{value:value,enumerable:true,configurable:true,writable:true})}else{obj[key]=value}return obj}import*as datalayer from"./datalayer";import*as mixpanel from"./mixpanel";import*as posthog from"./posthog";import*as logger from"./logger";export class InsightsService{initInsights({mixpanelToken,mixpanelAutoCapture,
|
|
1
|
+
function _define_property(obj,key,value){if(key in obj){Object.defineProperty(obj,key,{value:value,enumerable:true,configurable:true,writable:true})}else{obj[key]=value}return obj}import*as datalayer from"./datalayer";import*as mixpanel from"./mixpanel";import*as posthog from"./posthog";import*as logger from"./logger";export class InsightsService{initInsights({mixpanelToken,mixpanelAutoCapture,mixpanelApiHost,posthogApiKey,posthogApiHost,debug=false}){this.debugMode=!!debug;if(this.debugMode){logger.debug("InsightService: Initializing insights")}try{mixpanel.initMixpanel(mixpanelToken,mixpanelAutoCapture,this.debugMode,mixpanelApiHost)}catch(e){if(this.debugMode){logger.error("Failed to initialize Mixpanel",e)}}try{posthog.initPosthog(posthogApiKey,posthogApiHost)}catch(e){if(this.debugMode){logger.error("Failed to initialize Posthog",e)}}}enableDebugMode(){this.debugMode=true;logger.debug("Enabling debug mode");try{mixpanel.enableDebugMode();posthog.enableDebugMode()}catch(e){logger.error("Failed to enable debug mode",e)}}disableDebugMode(){this.debugMode=false;logger.debug("Disabling debug mode");try{mixpanel.disableDebugMode();posthog.disableDebugMode()}catch(e){logger.error("Failed to disable debug mode",e)}}identify(identity){const{userId,accountId,organisationId,email,name,...properties}=identity;if(!userId){if(this.debugMode){logger.warn("User ID not provided, skipping identify")}return}if(this.debugMode){logger.info("Identifying user",{userId,accountId,organisationId,email,name,...properties})}try{mixpanel.identify({userId,accountId,organisationId,email,name,...properties})}catch(e){if(this.debugMode){logger.error("Failed to identify user in Mixpanel",e)}}try{posthog.identify({userId,accountId,organisationId,email,name,...properties})}catch(e){if(this.debugMode){logger.error("Failed to identify user in Posthog",e)}}}trackPageView(options){const{excludeIds,includeDataLayer,...properties}=options??{};if(this.debugMode){logger.info("Tracking page view")}try{mixpanel.trackPageView({excludeIds,...properties})}catch(e){if(this.debugMode){logger.error("Failed to track page view in Mixpanel",e)}}try{posthog.trackPageView(properties)}catch(e){if(this.debugMode){logger.error("Failed to track page view in Posthog",e)}}if(includeDataLayer){try{datalayer.trackPageView(properties)}catch(e){if(this.debugMode){logger.error("Failed to track page view in GTM",e)}}}}track(event,properties){if(this.debugMode){logger.info("Tracking event",{event,properties})}try{mixpanel.track(event,properties)}catch(e){if(this.debugMode){logger.error("Failed to track event in Mixpanel",e)}}try{posthog.track(event,properties)}catch(e){if(this.debugMode){logger.error("Failed to track event in Posthog",e)}}try{datalayer.track(event,properties)}catch(e){if(this.debugMode){logger.error("Failed to track event in Datalayer",e)}}}setupObserver(){const getInsightAttributes=element=>{const MAX_ATTRIBUTES=10;let count=0;const attributes={};for(const attr of Array.from(element.attributes)){if(count>=MAX_ATTRIBUTES)break;if(attr.name.startsWith("data-insight-")){if(!/^data-insight-[a-zA-Z0-9-]+$/.test(attr.name))continue;if(typeof attr.value!=="string"||attr.value.length>100)continue;const key=attr.name.replace("data-insight-","").split("-").map((part,index)=>index===0?part:part.charAt(0).toUpperCase()+part.slice(1)).join("");attributes[key]=attr.value;count++}}return attributes};const findClosestElementWithInsights=element=>{let current=element;while(current&¤t!==document.body){const insights=getInsightAttributes(current);if(Object.keys(insights).length>0){return insights}current=current.parentElement}return null};const handleClick=event=>{if(!(event.target instanceof HTMLElement))return;const insights=findClosestElementWithInsights(event.target);if(insights){const{event:eventName,...properties}=insights;this.track(eventName||"element_clicked",properties)}};document.body.addEventListener("click",handleClick);return()=>{document.body.removeEventListener("click",handleClick)}}constructor(){_define_property(this,"debugMode",false)}}
|
|
2
2
|
//# sourceMappingURL=service.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/core/insights/service.ts"],"sourcesContent":["import type {\n AnalyticsService,\n InsightsConfig,\n InsightsIdentity,\n TrackPageViewOptions,\n} from \"./types\";\nimport * as datalayer from \"./datalayer\";\nimport * as mixpanel from \"./mixpanel\";\nimport * as posthog from \"./posthog\";\nimport * as logger from \"./logger\";\n\n// The real implementation that will be used after initialization\nexport class InsightsService implements AnalyticsService {\n private debugMode: boolean = false;\n\n initInsights({\n mixpanelToken,\n mixpanelAutoCapture,\n mixpanelRecordSessionsPercent = 1,\n posthogApiKey,\n posthogApiHost,\n debug = false,\n }: InsightsConfig): void {\n this.debugMode = !!debug;\n\n if (this.debugMode) {\n logger.debug(\"InsightService: Initializing insights\");\n }\n\n try {\n mixpanel.initMixpanel(\n mixpanelToken,\n mixpanelAutoCapture,\n this.debugMode,\n mixpanelRecordSessionsPercent,\n );\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to initialize Mixpanel\", e);\n }\n }\n\n try {\n posthog.initPosthog(posthogApiKey, posthogApiHost);\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to initialize Posthog\", e);\n }\n }\n }\n\n enableDebugMode(): void {\n this.debugMode = true;\n logger.debug(\"Enabling debug mode\");\n\n try {\n mixpanel.enableDebugMode();\n posthog.enableDebugMode();\n } catch (e) {\n logger.error(\"Failed to enable debug mode\", e);\n }\n }\n\n disableDebugMode(): void {\n this.debugMode = false;\n logger.debug(\"Disabling debug mode\");\n\n try {\n mixpanel.disableDebugMode();\n posthog.disableDebugMode();\n } catch (e) {\n logger.error(\"Failed to disable debug mode\", e);\n }\n }\n\n identify(identity: InsightsIdentity): void {\n const { userId, accountId, organisationId, email, name, ...properties } =\n identity;\n\n // In very rare cases we might have a user without an account, so we'll\n // let null/undefined/blank strings through on that one\n if (!userId) {\n if (this.debugMode) {\n logger.warn(\"User ID not provided, skipping identify\");\n }\n return;\n }\n\n if (this.debugMode) {\n logger.info(\"Identifying user\", {\n userId,\n accountId,\n organisationId,\n email,\n name,\n ...properties,\n });\n }\n\n try {\n mixpanel.identify({\n userId,\n accountId,\n organisationId,\n email,\n name,\n ...properties,\n });\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to identify user in Mixpanel\", e);\n }\n }\n\n try {\n posthog.identify({\n userId,\n accountId,\n organisationId,\n email,\n name,\n ...properties,\n });\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to identify user in Posthog\", e);\n }\n }\n }\n\n trackPageView(options?: TrackPageViewOptions): void {\n const { excludeIds, includeDataLayer, ...properties } = options ?? {};\n\n if (this.debugMode) {\n logger.info(\"Tracking page view\");\n }\n\n try {\n mixpanel.trackPageView({ excludeIds, ...properties });\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to track page view in Mixpanel\", e);\n }\n }\n\n try {\n posthog.trackPageView(properties);\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to track page view in Posthog\", e);\n }\n }\n\n if (includeDataLayer) {\n try {\n datalayer.trackPageView(properties);\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to track page view in GTM\", e);\n }\n }\n }\n }\n\n track(event: string, properties?: Record<string, unknown>): void {\n if (this.debugMode) {\n logger.info(\"Tracking event\", { event, properties });\n }\n\n try {\n mixpanel.track(event, properties);\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to track event in Mixpanel\", e);\n }\n }\n\n try {\n posthog.track(event, properties);\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to track event in Posthog\", e);\n }\n }\n\n try {\n datalayer.track(event, properties);\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to track event in Datalayer\", e);\n }\n }\n }\n\n startSessionRecording(): void {\n if (this.debugMode) {\n logger.info(\"Starting session recording\");\n }\n\n try {\n mixpanel.startSessionRecording();\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to start session recording in Mixpanel\", e);\n }\n }\n\n try {\n posthog.startSessionRecording();\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to start session recording in Posthog\", e);\n }\n }\n }\n\n stopSessionRecording(): void {\n if (this.debugMode) {\n logger.info(\"Stopping session recording\");\n }\n\n try {\n mixpanel.stopSessionRecording();\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to stop session recording in Mixpanel\", e);\n }\n }\n\n try {\n posthog.stopSessionRecording();\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to stop session recording in Posthog\", e);\n }\n }\n }\n\n setupObserver(): () => void {\n // Helper to get all data-insight-* attributes from an element\n const getInsightAttributes = (\n element: HTMLElement,\n ): { event?: string; [key: string]: string | undefined } => {\n // limit how many data attributes we'll process\n const MAX_ATTRIBUTES = 10;\n let count = 0;\n\n const attributes: { event?: string; [key: string]: string | undefined } =\n {};\n\n for (const attr of Array.from(element.attributes)) {\n if (count >= MAX_ATTRIBUTES) break;\n if (attr.name.startsWith(\"data-insight-\")) {\n // Validate attribute name format\n if (!/^data-insight-[a-zA-Z0-9-]+$/.test(attr.name)) continue;\n\n // Sanitize attribute value\n if (typeof attr.value !== \"string\" || attr.value.length > 100)\n continue;\n\n // Convert data-insight-event-name to eventName\n const key = attr.name\n .replace(\"data-insight-\", \"\")\n .split(\"-\")\n .map((part: string, index: number) =>\n index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1),\n )\n .join(\"\");\n attributes[key] = attr.value;\n count++;\n }\n }\n return attributes;\n };\n\n // Helper to find closest element with data-insight attributes\n const findClosestElementWithInsights = (element: HTMLElement) => {\n let current = element;\n while (current && current !== document.body) {\n const insights = getInsightAttributes(current);\n if (Object.keys(insights).length > 0) {\n return insights;\n }\n\n current = current.parentElement as HTMLElement;\n }\n return null;\n };\n\n // Global click handler\n const handleClick = (event: MouseEvent): void => {\n if (!(event.target instanceof HTMLElement)) return;\n const insights = findClosestElementWithInsights(event.target);\n if (insights) {\n // Extract special properties if they exist\n const { event: eventName, ...properties } = insights;\n this.track(eventName || \"element_clicked\", properties);\n }\n };\n\n // Add listener to document body to catch all clicks\n document.body.addEventListener(\"click\", handleClick);\n\n // Return cleanup function in case it's needed\n return () => {\n document.body.removeEventListener(\"click\", handleClick);\n };\n }\n}\n"],"names":["datalayer","mixpanel","posthog","logger","InsightsService","initInsights","mixpanelToken","mixpanelAutoCapture","mixpanelRecordSessionsPercent","posthogApiKey","posthogApiHost","debug","debugMode","initMixpanel","e","error","initPosthog","enableDebugMode","disableDebugMode","identify","identity","userId","accountId","organisationId","email","name","properties","warn","info","trackPageView","options","excludeIds","includeDataLayer","track","event","startSessionRecording","stopSessionRecording","setupObserver","getInsightAttributes","element","MAX_ATTRIBUTES","count","attributes","attr","Array","from","startsWith","test","value","length","key","replace","split","map","part","index","charAt","toUpperCase","slice","join","findClosestElementWithInsights","current","document","body","insights","Object","keys","parentElement","handleClick","target","HTMLElement","eventName","addEventListener","removeEventListener"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/core/insights/service.ts"],"sourcesContent":["import type {\n AnalyticsService,\n InsightsConfig,\n InsightsIdentity,\n TrackPageViewOptions,\n} from \"./types\";\nimport * as datalayer from \"./datalayer\";\nimport * as mixpanel from \"./mixpanel\";\nimport * as posthog from \"./posthog\";\nimport * as logger from \"./logger\";\n\n// The real implementation that will be used after initialization\nexport class InsightsService implements AnalyticsService {\n private debugMode: boolean = false;\n\n initInsights({\n mixpanelToken,\n mixpanelAutoCapture,\n mixpanelApiHost,\n posthogApiKey,\n posthogApiHost,\n debug = false,\n }: InsightsConfig): void {\n this.debugMode = !!debug;\n\n if (this.debugMode) {\n logger.debug(\"InsightService: Initializing insights\");\n }\n\n try {\n mixpanel.initMixpanel(\n mixpanelToken,\n mixpanelAutoCapture,\n this.debugMode,\n mixpanelApiHost,\n );\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to initialize Mixpanel\", e);\n }\n }\n\n try {\n posthog.initPosthog(posthogApiKey, posthogApiHost);\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to initialize Posthog\", e);\n }\n }\n }\n\n enableDebugMode(): void {\n this.debugMode = true;\n logger.debug(\"Enabling debug mode\");\n\n try {\n mixpanel.enableDebugMode();\n posthog.enableDebugMode();\n } catch (e) {\n logger.error(\"Failed to enable debug mode\", e);\n }\n }\n\n disableDebugMode(): void {\n this.debugMode = false;\n logger.debug(\"Disabling debug mode\");\n\n try {\n mixpanel.disableDebugMode();\n posthog.disableDebugMode();\n } catch (e) {\n logger.error(\"Failed to disable debug mode\", e);\n }\n }\n\n identify(identity: InsightsIdentity): void {\n const { userId, accountId, organisationId, email, name, ...properties } =\n identity;\n\n // In very rare cases we might have a user without an account, so we'll\n // let null/undefined/blank strings through on that one\n if (!userId) {\n if (this.debugMode) {\n logger.warn(\"User ID not provided, skipping identify\");\n }\n return;\n }\n\n if (this.debugMode) {\n logger.info(\"Identifying user\", {\n userId,\n accountId,\n organisationId,\n email,\n name,\n ...properties,\n });\n }\n\n try {\n mixpanel.identify({\n userId,\n accountId,\n organisationId,\n email,\n name,\n ...properties,\n });\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to identify user in Mixpanel\", e);\n }\n }\n\n try {\n posthog.identify({\n userId,\n accountId,\n organisationId,\n email,\n name,\n ...properties,\n });\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to identify user in Posthog\", e);\n }\n }\n }\n\n trackPageView(options?: TrackPageViewOptions): void {\n const { excludeIds, includeDataLayer, ...properties } = options ?? {};\n\n if (this.debugMode) {\n logger.info(\"Tracking page view\");\n }\n\n try {\n mixpanel.trackPageView({ excludeIds, ...properties });\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to track page view in Mixpanel\", e);\n }\n }\n\n try {\n posthog.trackPageView(properties);\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to track page view in Posthog\", e);\n }\n }\n\n if (includeDataLayer) {\n try {\n datalayer.trackPageView(properties);\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to track page view in GTM\", e);\n }\n }\n }\n }\n\n track(event: string, properties?: Record<string, unknown>): void {\n if (this.debugMode) {\n logger.info(\"Tracking event\", { event, properties });\n }\n\n try {\n mixpanel.track(event, properties);\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to track event in Mixpanel\", e);\n }\n }\n\n try {\n posthog.track(event, properties);\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to track event in Posthog\", e);\n }\n }\n\n try {\n datalayer.track(event, properties);\n } catch (e) {\n if (this.debugMode) {\n logger.error(\"Failed to track event in Datalayer\", e);\n }\n }\n }\n\n setupObserver(): () => void {\n // Helper to get all data-insight-* attributes from an element\n const getInsightAttributes = (\n element: HTMLElement,\n ): { event?: string; [key: string]: string | undefined } => {\n // limit how many data attributes we'll process\n const MAX_ATTRIBUTES = 10;\n let count = 0;\n\n const attributes: { event?: string; [key: string]: string | undefined } =\n {};\n\n for (const attr of Array.from(element.attributes)) {\n if (count >= MAX_ATTRIBUTES) break;\n if (attr.name.startsWith(\"data-insight-\")) {\n // Validate attribute name format\n if (!/^data-insight-[a-zA-Z0-9-]+$/.test(attr.name)) continue;\n\n // Sanitize attribute value\n if (typeof attr.value !== \"string\" || attr.value.length > 100)\n continue;\n\n // Convert data-insight-event-name to eventName\n const key = attr.name\n .replace(\"data-insight-\", \"\")\n .split(\"-\")\n .map((part: string, index: number) =>\n index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1),\n )\n .join(\"\");\n attributes[key] = attr.value;\n count++;\n }\n }\n return attributes;\n };\n\n // Helper to find closest element with data-insight attributes\n const findClosestElementWithInsights = (element: HTMLElement) => {\n let current = element;\n while (current && current !== document.body) {\n const insights = getInsightAttributes(current);\n if (Object.keys(insights).length > 0) {\n return insights;\n }\n\n current = current.parentElement as HTMLElement;\n }\n return null;\n };\n\n // Global click handler\n const handleClick = (event: MouseEvent): void => {\n if (!(event.target instanceof HTMLElement)) return;\n const insights = findClosestElementWithInsights(event.target);\n if (insights) {\n // Extract special properties if they exist\n const { event: eventName, ...properties } = insights;\n this.track(eventName || \"element_clicked\", properties);\n }\n };\n\n // Add listener to document body to catch all clicks\n document.body.addEventListener(\"click\", handleClick);\n\n // Return cleanup function in case it's needed\n return () => {\n document.body.removeEventListener(\"click\", handleClick);\n };\n }\n}\n"],"names":["datalayer","mixpanel","posthog","logger","InsightsService","initInsights","mixpanelToken","mixpanelAutoCapture","mixpanelApiHost","posthogApiKey","posthogApiHost","debug","debugMode","initMixpanel","e","error","initPosthog","enableDebugMode","disableDebugMode","identify","identity","userId","accountId","organisationId","email","name","properties","warn","info","trackPageView","options","excludeIds","includeDataLayer","track","event","setupObserver","getInsightAttributes","element","MAX_ATTRIBUTES","count","attributes","attr","Array","from","startsWith","test","value","length","key","replace","split","map","part","index","charAt","toUpperCase","slice","join","findClosestElementWithInsights","current","document","body","insights","Object","keys","parentElement","handleClick","target","HTMLElement","eventName","addEventListener","removeEventListener"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/core/insights/types.ts"],"sourcesContent":["export type InsightsConfig = {\n debug: boolean;\n mixpanelToken: string;\n mixpanelAutoCapture: boolean;\n
|
|
1
|
+
{"version":3,"sources":["../../../src/core/insights/types.ts"],"sourcesContent":["export type InsightsConfig = {\n debug: boolean;\n mixpanelToken: string;\n mixpanelAutoCapture: boolean;\n mixpanelApiHost?: string;\n posthogApiKey: string;\n posthogApiHost: string;\n};\n\n// Define the interface for our analytics service\nexport interface AnalyticsService {\n initInsights: (config: InsightsConfig) => void;\n enableDebugMode: () => void;\n disableDebugMode: () => void;\n identify: (identity: InsightsIdentity) => void;\n trackPageView: (options?: TrackPageViewOptions) => void;\n track: (event: string, properties?: Record<string, unknown>) => void;\n setupObserver: () => () => void;\n}\n\n// Command type for our queue\nexport type Command = {\n methodName: keyof AnalyticsService;\n args: unknown[];\n};\n\nexport type InsightsIdentity = {\n userId: string;\n accountId: string;\n organisationId?: string;\n email?: string;\n name?: string;\n} & Record<string, unknown>;\n\nexport type TrackPageViewOptions = {\n includeDataLayer?: boolean;\n excludeIds?: string[];\n} & Record<string, unknown>;\n"],"names":[],"mappings":"AAkCA,QAG4B"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import DOMPurify from"dompurify";const RELATIVE_URI=/^\/[^/\\]/;const DANGEROUS_URI_SCHEMES=/^\s*(data|javascript|vbscript|file|blob):/i;const URI_BEARING_ATTRS=new Set(["src","href","xlink:href","action","formaction","background","poster","srcset"]);DOMPurify.addHook("uponSanitizeAttribute",(_node,data)=>{if(URI_BEARING_ATTRS.has(data.attrName)&&DANGEROUS_URI_SCHEMES.test(data.attrValue)){data.keepAttr=false}});export const sanitizeInlineMarkup=input=>DOMPurify.sanitize(input??"",{ALLOWED_TAGS:["a"],ALLOWED_ATTR:["href","data-method"],ALLOWED_URI_REGEXP:RELATIVE_URI});export const sanitizeRichText=input=>DOMPurify.sanitize(input??"",{ALLOWED_TAGS:["a","b","br","em","i","p","strong"],ALLOWED_ATTR:["href"],ALLOWED_URI_REGEXP:RELATIVE_URI});export const sanitizeMarketingHtml=input=>DOMPurify.sanitize(input??"",{ALLOWED_TAGS:["a","b","blockquote","br","code","em","figcaption","figure","h2","h3","h4","i","img","li","ol","p","pre","span","strong","table","tbody","td","th","thead","tr","ul"],ALLOWED_ATTR:["alt","class","href","src","title"],ALLOWED_URI_REGEXP:/^(https?:\/\/|\/[^/\\])/});
|
|
2
|
+
//# sourceMappingURL=sanitize-html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/core/utils/sanitize-html.ts"],"sourcesContent":["import DOMPurify from \"dompurify\";\n\n// Restricts hrefs to same-origin paths (no scheme, no protocol-relative).\n// Matches the rule applied historically in Flash and Notice, with one\n// hardening: the exclusion class also rejects backslash. Per WHATWG URL,\n// browsers parsing a special-scheme URL treat `/\\` as an authority delimiter\n// identical to `//`, so `href=\"/\\evil.com\"` resolves to `http://evil.com`.\n// DOMPurify checks ALLOWED_URI_REGEXP against the raw attribute string, not\n// the resolved URL, so the exclusion has to do the work.\nconst RELATIVE_URI = /^\\/[^/\\\\]/;\n\n// DOMPurify hard-codes a data: URI allowance on `img`, `audio`, `video`,\n// `source`, `image`, and `track` regardless of ALLOWED_URI_REGEXP - intended\n// to support inline base64 images. We don't want that exception: a\n// `data:image/svg+xml,<svg onload=alert(1)>` URI on an <img> still executes\n// in some contexts, and we'd rather force authors to host their images. The\n// hook below catches dangerous schemes on every URI-bearing attribute and\n// drops the attribute, regardless of which built-in safe-list DOMPurify\n// would normally apply.\nconst DANGEROUS_URI_SCHEMES = /^\\s*(data|javascript|vbscript|file|blob):/i;\nconst URI_BEARING_ATTRS = new Set([\n \"src\",\n \"href\",\n \"xlink:href\",\n \"action\",\n \"formaction\",\n \"background\",\n \"poster\",\n \"srcset\",\n]);\n\nDOMPurify.addHook(\"uponSanitizeAttribute\", (_node, data) => {\n if (\n URI_BEARING_ATTRS.has(data.attrName) &&\n DANGEROUS_URI_SCHEMES.test(data.attrValue)\n ) {\n data.keepAttr = false;\n }\n});\n\n/**\n * sanitizeInlineMarkup — tightest allowlist, intended for short pieces of\n * trusted-but-defence-in-depthed text (flash messages, banner bodies that\n * already went through Rails sanitisation). Only inline links to same-origin\n * paths survive.\n *\n * Use this for surfaces where the producer (Rails helper, backend flash)\n * already sanitised the input and React-side sanitisation is the second\n * layer of defence.\n */\nexport const sanitizeInlineMarkup = (input: string | null | undefined) =>\n DOMPurify.sanitize(input ?? \"\", {\n ALLOWED_TAGS: [\"a\"],\n ALLOWED_ATTR: [\"href\", \"data-method\"],\n ALLOWED_URI_REGEXP: RELATIVE_URI,\n });\n\n/**\n * sanitizeRichText — mirror of DashboardNoticeHelper::SANITISE_TAGS on the\n * Rails side. Use for content that an admin types as light HTML (banner\n * body text, dashboard notices). Keeps href so links work; same-origin URI\n * restriction prevents the URI from carrying a script payload.\n *\n * Tag set deliberately matches the server-side allowlist so the trust\n * boundary is identical regardless of which side did the rendering.\n */\nexport const sanitizeRichText = (input: string | null | undefined) =>\n DOMPurify.sanitize(input ?? \"\", {\n ALLOWED_TAGS: [\"a\", \"b\", \"br\", \"em\", \"i\", \"p\", \"strong\"],\n ALLOWED_ATTR: [\"href\"],\n ALLOWED_URI_REGEXP: RELATIVE_URI,\n });\n\n/**\n * sanitizeMarketingHtml — wider allowlist for Contentful-sourced marketing\n * content (the `ContentfulBlockHtml` escape-hatch field). Trust boundary is\n * \"compromised Contentful editor account\" — wider than inline/rich-text but\n * still blocks scripts, event handlers, and non-http(s) URIs.\n *\n * Not suitable for the Ghost blog body. A survey of all 396 published Ably\n * blog posts (built locally against Ghost) shows the body legitimately\n * contains HubSpot CTA scripts (~70 posts), Twitter widget scripts, YouTube\n * and Vimeo iframes (~50), and inline SVG diagrams (~130). Sanitising those\n * with this allowlist would silently break the site. Ghost body is treated\n * as a trusted-CMS surface in the ADR — editor account control is the\n * security boundary.\n *\n * Marketing copy needs headings, lists, code blocks, blockquotes, and\n * external links, so the URI rule allows http(s) instead of being clamped\n * to relative paths.\n */\nexport const sanitizeMarketingHtml = (input: string | null | undefined) =>\n DOMPurify.sanitize(input ?? \"\", {\n ALLOWED_TAGS: [\n \"a\",\n \"b\",\n \"blockquote\",\n \"br\",\n \"code\",\n \"em\",\n \"figcaption\",\n \"figure\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"i\",\n \"img\",\n \"li\",\n \"ol\",\n \"p\",\n \"pre\",\n \"span\",\n \"strong\",\n \"table\",\n \"tbody\",\n \"td\",\n \"th\",\n \"thead\",\n \"tr\",\n \"ul\",\n ],\n ALLOWED_ATTR: [\"alt\", \"class\", \"href\", \"src\", \"title\"],\n // Either a full http(s):// URL, or a same-origin relative path whose\n // second character is neither `/` nor `\\`. The earlier `/^(https?:)?\\//`\n // was anchored only at the start, so `//evil.com` (protocol-relative)\n // survived: the regex matched the leading `/` and left the rest\n // unanchored. Browsers resolve `//evil.com` to the page scheme, so this\n // was an external-redirect bypass exactly in the threat model the\n // marketing sanitiser is meant to harden against (compromised Contentful\n // editor sneaking off-site links past review).\n ALLOWED_URI_REGEXP: /^(https?:\\/\\/|\\/[^/\\\\])/,\n });\n"],"names":["DOMPurify","RELATIVE_URI","DANGEROUS_URI_SCHEMES","URI_BEARING_ATTRS","Set","addHook","_node","data","has","attrName","test","attrValue","keepAttr","sanitizeInlineMarkup","input","sanitize","ALLOWED_TAGS","ALLOWED_ATTR","ALLOWED_URI_REGEXP","sanitizeRichText","sanitizeMarketingHtml"],"mappings":"AAAA,OAAOA,cAAe,WAAY,CASlC,MAAMC,aAAe,YAUrB,MAAMC,sBAAwB,6CAC9B,MAAMC,kBAAoB,IAAIC,IAAI,CAChC,MACA,OACA,aACA,SACA,aACA,aACA,SACA,SACD,EAEDJ,UAAUK,OAAO,CAAC,wBAAyB,CAACC,MAAOC,QACjD,GACEJ,kBAAkBK,GAAG,CAACD,KAAKE,QAAQ,GACnCP,sBAAsBQ,IAAI,CAACH,KAAKI,SAAS,EACzC,CACAJ,KAAKK,QAAQ,CAAG,KAClB,CACF,EAYA,QAAO,MAAMC,qBAAuB,AAACC,OACnCd,UAAUe,QAAQ,CAACD,OAAS,GAAI,CAC9BE,aAAc,CAAC,IAAI,CACnBC,aAAc,CAAC,OAAQ,cAAc,CACrCC,mBAAoBjB,YACtB,EAAG,AAWL,QAAO,MAAMkB,iBAAmB,AAACL,OAC/Bd,UAAUe,QAAQ,CAACD,OAAS,GAAI,CAC9BE,aAAc,CAAC,IAAK,IAAK,KAAM,KAAM,IAAK,IAAK,SAAS,CACxDC,aAAc,CAAC,OAAO,CACtBC,mBAAoBjB,YACtB,EAAG,AAoBL,QAAO,MAAMmB,sBAAwB,AAACN,OACpCd,UAAUe,QAAQ,CAACD,OAAS,GAAI,CAC9BE,aAAc,CACZ,IACA,IACA,aACA,KACA,OACA,KACA,aACA,SACA,KACA,KACA,KACA,IACA,MACA,KACA,KACA,IACA,MACA,OACA,SACA,QACA,QACA,KACA,KACA,QACA,KACA,KACD,CACDC,aAAc,CAAC,MAAO,QAAS,OAAQ,MAAO,QAAQ,CAStDC,mBAAoB,yBACtB,EAAG"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{describe,expect,it}from"vitest";import{sanitizeInlineMarkup,sanitizeMarketingHtml,sanitizeRichText}from"./sanitize-html";const parseDom=html=>{const wrapper=document.createElement("div");wrapper.innerHTML=html;return wrapper};const eventHandlerAttrsOf=root=>{const offenders=[];root.querySelectorAll("*").forEach(el=>{for(const attr of Array.from(el.attributes)){if(attr.name.toLowerCase().startsWith("on")){offenders.push(`${el.tagName}:${attr.name}`)}}});return offenders};const dangerousProtocols=/^(javascript|data|vbscript|file|blob):/i;const dangerousHrefsIn=root=>{const offenders=[];root.querySelectorAll("[href]").forEach(el=>{if(dangerousProtocols.test(el.getAttribute("href")??"")){offenders.push(el.getAttribute("href")??"")}});root.querySelectorAll("[src]").forEach(el=>{if(dangerousProtocols.test(el.getAttribute("src")??"")){offenders.push(el.getAttribute("src")??"")}});return offenders};const scriptTagsIn=root=>Array.from(root.querySelectorAll("script"));describe("sanitizeInlineMarkup",()=>{describe("baseline allowlist behaviour",()=>{it("keeps same-origin relative links",()=>{expect(sanitizeInlineMarkup('<a href="/dashboard">go</a>')).toBe('<a href="/dashboard">go</a>')});it("keeps a data-method attribute on a link (Rails UJS)",()=>{const out=sanitizeInlineMarkup('<a href="/logout" data-method="delete">Log out</a>');expect(out).toContain('data-method="delete"')});it("strips disallowed tags but keeps their text content",()=>{expect(sanitizeInlineMarkup("<b>bold</b>")).toBe("bold")});it("returns an empty string for null and undefined",()=>{expect(sanitizeInlineMarkup(null)).toBe("");expect(sanitizeInlineMarkup(undefined)).toBe("")});it("returns an empty string for empty input",()=>{expect(sanitizeInlineMarkup("")).toBe("")})});describe("script-tag injection",()=>{it.each(["<script>alert(1)</script>","<SCRIPT>alert(1)</SCRIPT>","<ScRiPt>alert(1)</ScRiPt>","<script src=//evil.com/x.js></script>","<script\nsrc=//evil.com/x.js></script>","<script type='text/javascript'>alert(1)</script>","<script >alert(1)</script>"])("strips '%s'",payload=>{const out=sanitizeInlineMarkup(payload+"safe");expect(scriptTagsIn(parseDom(out))).toHaveLength(0);expect(out).toContain("safe")})});describe("event-handler injection",()=>{it.each(['<a href="/x" onmouseover="alert(1)">l</a>','<a href="/x" onclick="alert(1)">l</a>','<a href="/x" onfocus="alert(1)" autofocus>l</a>','<a href="/x" oNmOuSeOvEr="alert(1)">l</a>',"<a href=\"/x\" onmouseover='alert(1)'>l</a>","<a href=/x onmouseover=alert(1)>l</a>","<a href=/x onmouseover=alert(1)>l</a>","<a href=/x\nonmouseover=alert(1)>l</a>","<a href=/x onmouseover=`alert(1)`>l</a>"])("strips event handlers in '%s'",payload=>{const out=sanitizeInlineMarkup(payload);expect(eventHandlerAttrsOf(parseDom(out))).toEqual([])})});describe("javascript:/data:/vbscript: URI bypasses",()=>{it.each(["javascript:alert(1)","JaVaScRiPt:alert(1)","java script:alert(1)","java\nscript:alert(1)","java\rscript:alert(1)","java\0script:alert(1)","javascript:alert(1)","javascript:alert(1)","javascript:alert(1)","vbscript:msgbox(1)","data:text/html,<script>alert(1)</script>","data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==","file:///etc/passwd","blob:https://example.com/abc"])("strips href='%s'",href=>{const out=sanitizeInlineMarkup(`<a href="${href}">x</a>`);expect(dangerousHrefsIn(parseDom(out))).toEqual([])})});describe("authority-delimiter / protocol-relative bypasses",()=>{it.each(["//evil.com","///evil.com","/\\evil.com","/\\\\evil.com","/\\/evil.com","/%5cevil.com"])("strips href='%s'",href=>{const out=sanitizeInlineMarkup(`<a href="${href}">click</a>`);const root=parseDom(out);root.querySelectorAll("a[href]").forEach(a=>{const raw=a.getAttribute("href")??"";expect(raw).toMatch(/^\/[^/\\]/)})})});describe("DOMPurify historical mXSS / mutation vectors",()=>{it("strips noscript-wrapped HTML injection",()=>{const payload='<noscript><p title="</noscript><img src=x onerror=alert(1)>">';const out=sanitizeInlineMarkup(payload);const root=parseDom(out);expect(scriptTagsIn(root)).toHaveLength(0);expect(eventHandlerAttrsOf(root)).toEqual([]);expect(root.querySelector("img")).toBeNull()});it("strips template / svg foreignObject smuggling",()=>{const payload="<template><svg><foreignObject><body onload=alert(1)>x</body></foreignObject></svg></template>";const out=sanitizeInlineMarkup(payload);expect(eventHandlerAttrsOf(parseDom(out))).toEqual([])});it("strips MathML annotation-xml encoded HTML",()=>{const payload='<math><annotation-xml encoding="text/html"><iframe src="javascript:alert(1)"></iframe></annotation-xml></math>';const out=sanitizeInlineMarkup(payload);const root=parseDom(out);expect(root.querySelector("iframe")).toBeNull();expect(dangerousHrefsIn(root)).toEqual([])})})});describe("sanitizeRichText",()=>{describe("baseline allowlist behaviour",()=>{it("keeps the allowlisted inline tags",()=>{const input='<p>Hello <strong>world</strong>, <em>see</em> <a href="/x">x</a><br>and <b>more</b>.</p>';expect(sanitizeRichText(input)).toBe(input)});it("strips img tags (not in allowlist) but keeps trailing text",()=>{const out=sanitizeRichText('<img src=x onerror="alert(1)">trail');const root=parseDom(out);expect(root.querySelector("img")).toBeNull();expect(eventHandlerAttrsOf(root)).toEqual([]);expect(out).toContain("trail")})});describe("script + event-handler corpus (symmetric with sanitizeInlineMarkup)",()=>{it.each(["<script>alert(1)</script>","<SCRIPT>alert(1)</SCRIPT>",'<p onclick="alert(1)">x</p>','<a href="/x" onmouseover="alert(1)">l</a>','<strong onerror="alert(1)">x</strong>'])("strips '%s'",payload=>{const out=sanitizeRichText(payload+"tail");const root=parseDom(out);expect(scriptTagsIn(root)).toHaveLength(0);expect(eventHandlerAttrsOf(root)).toEqual([]);expect(out).toContain("tail")})});describe("URI bypasses (symmetric with sanitizeInlineMarkup)",()=>{it.each(["javascript:alert(1)","JaVaScRiPt:alert(1)","java script:alert(1)","javascript:alert(1)","javascript:alert(1)","data:text/html,<script>alert(1)</script>","vbscript:msgbox(1)","https://evil.com","//evil.com","///evil.com","/\\evil.com","/\\\\evil.com"])("strips href='%s'",href=>{const out=sanitizeRichText(`<a href="${href}">x</a>`);const root=parseDom(out);expect(dangerousHrefsIn(root)).toEqual([]);root.querySelectorAll("a[href]").forEach(a=>{expect(a.getAttribute("href")).toMatch(/^\/[^/\\]/)})})});describe("disallowed-but-tempting tags",()=>{it.each(["<iframe src=javascript:alert(1)></iframe>","<object data=javascript:alert(1)></object>","<embed src=javascript:alert(1)></embed>","<svg><script>alert(1)</script></svg>","<svg onload=alert(1)></svg>","<details ontoggle=alert(1) open>x</details>","<marquee onstart=alert(1)>x</marquee>","<video><source onerror=alert(1)></video>","<form><input type=image src=x onerror=alert(1)></form>","<isindex action=javascript:alert(1) type=image>","<base href=javascript:alert(1)//>","<meta http-equiv=refresh content=0;url=javascript:alert(1)>"])("strips '%s'",payload=>{const out=sanitizeRichText(payload+"ok");const root=parseDom(out);expect(scriptTagsIn(root)).toHaveLength(0);expect(eventHandlerAttrsOf(root)).toEqual([]);expect(dangerousHrefsIn(root)).toEqual([]);["iframe","object","embed","svg","details","marquee","video","source","form","input","isindex","base","meta"].forEach(t=>expect(root.querySelector(t)).toBeNull())})})});describe("sanitizeMarketingHtml",()=>{describe("baseline allowlist behaviour",()=>{it("keeps headings, lists, code, blockquote",()=>{const input="<h2>Title</h2><ul><li>One</li></ul><pre><code>code</code></pre><blockquote>quote</blockquote>";expect(sanitizeMarketingHtml(input)).toBe(input)});it("allows external https links",()=>{const out=sanitizeMarketingHtml('<a href="https://ably.com">go</a>');expect(out).toContain('href="https://ably.com"')});it("allows http and relative URIs on img src",()=>{const out=sanitizeMarketingHtml('<img src="https://ably.com/x.png" alt="x">');expect(out).toContain('src="https://ably.com/x.png"')});it("preserves figure/figcaption around images (Ghost-style)",()=>{const input='<figure><img src="/x.png" alt="x"><figcaption>caption</figcaption></figure>';expect(sanitizeMarketingHtml(input)).toBe(input)})});describe("script and embed stripping",()=>{it.each(["<script>alert(1)</script>",'<script src="https://js.hscta.net/cta/current.js"></script>','<script type="application/ld+json">{"x":1}</script>',"<iframe src=https://youtube.com></iframe>","<iframe src=javascript:alert(1)></iframe>",'<object data="https://evil.com"></object>',"<embed src=javascript:alert(1)>","<form action=javascript:alert(1)><input></form>","<svg><script>alert(1)</script></svg>","<svg onload=alert(1)></svg>","<math><mtext><img src=x onerror=alert(1)></mtext></math>"])("strips '%s' while keeping trailing copy",payload=>{const out=sanitizeMarketingHtml(payload+"copy");const root=parseDom(out);expect(scriptTagsIn(root)).toHaveLength(0);expect(eventHandlerAttrsOf(root)).toEqual([]);expect(dangerousHrefsIn(root)).toEqual([]);["iframe","object","embed","form","input","svg","script","math"].forEach(t=>expect(root.querySelector(t)).toBeNull());expect(out).toContain("copy")})});describe("URL-protocol bypasses on allowed-tag attributes",()=>{it.each(["javascript:alert(1)","JaVaScRiPt:alert(1)","java script:alert(1)","java\nscript:alert(1)","javascript:alert(1)","javascript:alert(1)","javascript:alert(1)","data:text/html,<script>alert(1)</script>","vbscript:msgbox(1)","file:///etc/passwd","blob:https://example.com/abc"])("strips href='%s' on <a>",href=>{const out=sanitizeMarketingHtml(`<a href="${href}">x</a>`);expect(dangerousHrefsIn(parseDom(out))).toEqual([])});it.each(["javascript:alert(1)","data:image/svg+xml,<svg onload=alert(1)>","vbscript:msgbox(1)"])("strips img src='%s'",src=>{const out=sanitizeMarketingHtml(`<img src="${src}" alt="x">`);expect(dangerousHrefsIn(parseDom(out))).toEqual([])})});describe("authority-delimiter / protocol-relative bypasses",()=>{it.each(["//evil.com","///evil.com","/\\evil.com","/\\\\evil.com","/\\/evil.com","/%5cevil.com"])("strips href='%s' on <a>",href=>{const out=sanitizeMarketingHtml(`<a href="${href}">click</a>`);const root=parseDom(out);root.querySelectorAll("a[href]").forEach(a=>{const raw=a.getAttribute("href")??"";expect(raw).toMatch(/^(https?:\/\/|\/[^/\\])/)})});it.each(["//evil.com/x.png","/\\evil.com/x.png","/\\\\evil.com/x.png"])("strips img src='%s'",src=>{const out=sanitizeMarketingHtml(`<img src="${src}" alt="x">`);const root=parseDom(out);root.querySelectorAll("img[src]").forEach(img=>{expect(img.getAttribute("src")).toMatch(/^(https?:\/\/|\/[^/\\])/)})})});describe("style-based payloads",()=>{it("strips style attribute entirely (we do not allow it)",()=>{const out=sanitizeMarketingHtml('<p style="background:url(javascript:alert(1))">x</p>');expect(out).not.toContain("style");expect(out).not.toContain("javascript:")});it("strips inline style with expression()",()=>{const out=sanitizeMarketingHtml('<p style="width:expression(alert(1))">x</p>');expect(out).not.toContain("style");expect(out).not.toContain("expression")})});describe("event-handler corpus on allowed tags",()=>{it.each(['<p onclick="alert(1)">x</p>','<a href="/x" onmouseover="alert(1)">l</a>','<img src="/x.png" onerror="alert(1)" alt="x">','<h2 onmouseenter="alert(1)">t</h2>','<table onclick="alert(1)"><tbody><tr><td>x</td></tr></tbody></table>'])("strips event handlers in '%s'",payload=>{const out=sanitizeMarketingHtml(payload);expect(eventHandlerAttrsOf(parseDom(out))).toEqual([])})});describe("mXSS / parser-confusion vectors",()=>{it("strips noscript-wrapped img injection",()=>{const payload='<noscript><p title="</noscript><img src=x onerror=alert(1)>">';const out=sanitizeMarketingHtml(payload);const root=parseDom(out);expect(eventHandlerAttrsOf(root)).toEqual([]);const img=root.querySelector("img");if(img){expect(img.getAttribute("onerror")).toBeNull();expect(img.getAttribute("src")).not.toBe("x")}});it("strips xlink:href javascript on svg use",()=>{const out=sanitizeMarketingHtml('<svg><use xlink:href="javascript:alert(1)"></use></svg>');const root=parseDom(out);expect(root.querySelector("svg")).toBeNull();expect(dangerousHrefsIn(root)).toEqual([])});it("strips title-attribute escape inside an allowed tag",()=>{const payload='<a href="/x" title=\'"><script>alert(1)</script><p \'>link</a>';const out=sanitizeMarketingHtml(payload);expect(scriptTagsIn(parseDom(out))).toHaveLength(0)})})});
|
|
2
|
+
//# sourceMappingURL=sanitize-html.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/core/utils/sanitize-html.test.ts"],"sourcesContent":["/**\n * @vitest-environment jsdom\n *\n * Adversarial test corpus for the sanitize-html primitives. Cases are sourced\n * from public XSS cheatsheets (OWASP, PortSwigger, html5sec.org) and from\n * past DOMPurify CVEs / advisories. They exercise the wrappers, not DOMPurify\n * itself — the assertion in every case is \"the rendered string does not\n * carry an executable payload through to a DOM that would fire on hydration\".\n *\n * The shape of each assertion is intentionally strict: we don't trust that\n * \"alert\" or \"javascript\" not appearing as a substring means safety, so most\n * checks combine \"tag/attr stripped\" + \"no live-handler attribute survives\"\n * + \"raw text content may survive but is rendered as text\".\n */\n\nimport { describe, expect, it } from \"vitest\";\n\nimport {\n sanitizeInlineMarkup,\n sanitizeMarketingHtml,\n sanitizeRichText,\n} from \"./sanitize-html\";\n\n// Helper: parse the sanitised string into a real DOM, then assert no element\n// in the resulting tree carries an attribute whose name starts with `on`,\n// nor an `href`/`src` whose value resolves to anything beyond the allowlist.\nconst parseDom = (html: string) => {\n const wrapper = document.createElement(\"div\");\n wrapper.innerHTML = html;\n return wrapper;\n};\n\nconst eventHandlerAttrsOf = (root: HTMLElement) => {\n const offenders: string[] = [];\n root.querySelectorAll(\"*\").forEach((el) => {\n for (const attr of Array.from(el.attributes)) {\n if (attr.name.toLowerCase().startsWith(\"on\")) {\n offenders.push(`${el.tagName}:${attr.name}`);\n }\n }\n });\n return offenders;\n};\n\nconst dangerousProtocols = /^(javascript|data|vbscript|file|blob):/i;\nconst dangerousHrefsIn = (root: HTMLElement) => {\n const offenders: string[] = [];\n root.querySelectorAll<HTMLAnchorElement>(\"[href]\").forEach((el) => {\n if (dangerousProtocols.test(el.getAttribute(\"href\") ?? \"\")) {\n offenders.push(el.getAttribute(\"href\") ?? \"\");\n }\n });\n root.querySelectorAll<HTMLImageElement>(\"[src]\").forEach((el) => {\n if (dangerousProtocols.test(el.getAttribute(\"src\") ?? \"\")) {\n offenders.push(el.getAttribute(\"src\") ?? \"\");\n }\n });\n return offenders;\n};\n\nconst scriptTagsIn = (root: HTMLElement) =>\n Array.from(root.querySelectorAll(\"script\"));\n\ndescribe(\"sanitizeInlineMarkup\", () => {\n describe(\"baseline allowlist behaviour\", () => {\n it(\"keeps same-origin relative links\", () => {\n expect(sanitizeInlineMarkup('<a href=\"/dashboard\">go</a>')).toBe(\n '<a href=\"/dashboard\">go</a>',\n );\n });\n\n it(\"keeps a data-method attribute on a link (Rails UJS)\", () => {\n const out = sanitizeInlineMarkup(\n '<a href=\"/logout\" data-method=\"delete\">Log out</a>',\n );\n expect(out).toContain('data-method=\"delete\"');\n });\n\n it(\"strips disallowed tags but keeps their text content\", () => {\n expect(sanitizeInlineMarkup(\"<b>bold</b>\")).toBe(\"bold\");\n });\n\n it(\"returns an empty string for null and undefined\", () => {\n expect(sanitizeInlineMarkup(null)).toBe(\"\");\n expect(sanitizeInlineMarkup(undefined)).toBe(\"\");\n });\n\n it(\"returns an empty string for empty input\", () => {\n expect(sanitizeInlineMarkup(\"\")).toBe(\"\");\n });\n });\n\n describe(\"script-tag injection\", () => {\n it.each([\n \"<script>alert(1)</script>\",\n \"<SCRIPT>alert(1)</SCRIPT>\",\n \"<ScRiPt>alert(1)</ScRiPt>\",\n \"<script src=//evil.com/x.js></script>\",\n \"<script\\nsrc=//evil.com/x.js></script>\",\n \"<script type='text/javascript'>alert(1)</script>\",\n \"<script\t>alert(1)</script>\",\n ])(\"strips '%s'\", (payload) => {\n const out = sanitizeInlineMarkup(payload + \"safe\");\n expect(scriptTagsIn(parseDom(out))).toHaveLength(0);\n expect(out).toContain(\"safe\");\n });\n });\n\n describe(\"event-handler injection\", () => {\n it.each([\n '<a href=\"/x\" onmouseover=\"alert(1)\">l</a>',\n '<a href=\"/x\" onclick=\"alert(1)\">l</a>',\n '<a href=\"/x\" onfocus=\"alert(1)\" autofocus>l</a>',\n '<a href=\"/x\" oNmOuSeOvEr=\"alert(1)\">l</a>',\n \"<a href=\\\"/x\\\" onmouseover='alert(1)'>l</a>\",\n // Unquoted attribute value\n \"<a href=/x onmouseover=alert(1)>l</a>\",\n // Tab between attribute name and equals\n \"<a\\thref=/x\\tonmouseover=alert(1)>l</a>\",\n // Newline between attribute name and value\n \"<a href=/x\\nonmouseover=alert(1)>l</a>\",\n // Backtick around value (some parsers accept)\n \"<a href=/x onmouseover=`alert(1)`>l</a>\",\n ])(\"strips event handlers in '%s'\", (payload) => {\n const out = sanitizeInlineMarkup(payload);\n expect(eventHandlerAttrsOf(parseDom(out))).toEqual([]);\n });\n });\n\n describe(\"javascript:/data:/vbscript: URI bypasses\", () => {\n it.each([\n \"javascript:alert(1)\",\n \"JaVaScRiPt:alert(1)\",\n // Tab in scheme (browsers normalise away whitespace inside URLs)\n \"java\\tscript:alert(1)\",\n // Newline in scheme\n \"java\\nscript:alert(1)\",\n // Carriage-return in scheme\n \"java\\rscript:alert(1)\",\n // Null byte in scheme\n \"java\\0script:alert(1)\",\n // HTML-entity-encoded colon\n \"javascript:alert(1)\",\n // HTML decimal entity\n \"javascript:alert(1)\",\n // Long form decimal\n \"javascript:alert(1)\",\n // vbscript\n \"vbscript:msgbox(1)\",\n // data: with embedded script\n \"data:text/html,<script>alert(1)</script>\",\n \"data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==\",\n // file:\n \"file:///etc/passwd\",\n // blob:\n \"blob:https://example.com/abc\",\n ])(\"strips href='%s'\", (href) => {\n const out = sanitizeInlineMarkup(`<a href=\"${href}\">x</a>`);\n expect(dangerousHrefsIn(parseDom(out))).toEqual([]);\n });\n });\n\n describe(\"authority-delimiter / protocol-relative bypasses\", () => {\n it.each([\n \"//evil.com\",\n \"///evil.com\",\n \"/\\\\evil.com\", // CRITICAL: slash-backslash is authority-delimiter in WHATWG\n \"/\\\\\\\\evil.com\", // slash + two backslashes\n \"/\\\\/evil.com\", // mixed slash-backslash\n // URL-encoded backslash - browsers may or may not normalise\n \"/%5cevil.com\",\n ])(\"strips href='%s'\", (href) => {\n const out = sanitizeInlineMarkup(`<a href=\"${href}\">click</a>`);\n const root = parseDom(out);\n // Either the href is gone entirely, or it doesn't resolve to a foreign host\n root.querySelectorAll<HTMLAnchorElement>(\"a[href]\").forEach((a) => {\n const raw = a.getAttribute(\"href\") ?? \"\";\n // A surviving href must start with a single slash followed by a non-slash,\n // non-backslash character.\n expect(raw).toMatch(/^\\/[^/\\\\]/);\n });\n });\n });\n\n describe(\"DOMPurify historical mXSS / mutation vectors\", () => {\n // Mutation XSS via noscript - DOMPurify has had advisories around these\n it(\"strips noscript-wrapped HTML injection\", () => {\n const payload =\n '<noscript><p title=\"</noscript><img src=x onerror=alert(1)>\">';\n const out = sanitizeInlineMarkup(payload);\n const root = parseDom(out);\n expect(scriptTagsIn(root)).toHaveLength(0);\n expect(eventHandlerAttrsOf(root)).toEqual([]);\n expect(root.querySelector(\"img\")).toBeNull();\n });\n\n it(\"strips template / svg foreignObject smuggling\", () => {\n const payload =\n \"<template><svg><foreignObject><body onload=alert(1)>x</body></foreignObject></svg></template>\";\n const out = sanitizeInlineMarkup(payload);\n expect(eventHandlerAttrsOf(parseDom(out))).toEqual([]);\n });\n\n it(\"strips MathML annotation-xml encoded HTML\", () => {\n const payload =\n '<math><annotation-xml encoding=\"text/html\"><iframe src=\"javascript:alert(1)\"></iframe></annotation-xml></math>';\n const out = sanitizeInlineMarkup(payload);\n const root = parseDom(out);\n expect(root.querySelector(\"iframe\")).toBeNull();\n expect(dangerousHrefsIn(root)).toEqual([]);\n });\n });\n});\n\ndescribe(\"sanitizeRichText\", () => {\n describe(\"baseline allowlist behaviour\", () => {\n it(\"keeps the allowlisted inline tags\", () => {\n const input =\n '<p>Hello <strong>world</strong>, <em>see</em> <a href=\"/x\">x</a><br>and <b>more</b>.</p>';\n expect(sanitizeRichText(input)).toBe(input);\n });\n\n it(\"strips img tags (not in allowlist) but keeps trailing text\", () => {\n const out = sanitizeRichText('<img src=x onerror=\"alert(1)\">trail');\n const root = parseDom(out);\n expect(root.querySelector(\"img\")).toBeNull();\n expect(eventHandlerAttrsOf(root)).toEqual([]);\n expect(out).toContain(\"trail\");\n });\n });\n\n describe(\"script + event-handler corpus (symmetric with sanitizeInlineMarkup)\", () => {\n it.each([\n \"<script>alert(1)</script>\",\n \"<SCRIPT>alert(1)</SCRIPT>\",\n '<p onclick=\"alert(1)\">x</p>',\n '<a href=\"/x\" onmouseover=\"alert(1)\">l</a>',\n '<strong onerror=\"alert(1)\">x</strong>',\n ])(\"strips '%s'\", (payload) => {\n const out = sanitizeRichText(payload + \"tail\");\n const root = parseDom(out);\n expect(scriptTagsIn(root)).toHaveLength(0);\n expect(eventHandlerAttrsOf(root)).toEqual([]);\n expect(out).toContain(\"tail\");\n });\n });\n\n describe(\"URI bypasses (symmetric with sanitizeInlineMarkup)\", () => {\n it.each([\n \"javascript:alert(1)\",\n \"JaVaScRiPt:alert(1)\",\n \"java\\tscript:alert(1)\",\n \"javascript:alert(1)\",\n \"javascript:alert(1)\",\n \"data:text/html,<script>alert(1)</script>\",\n \"vbscript:msgbox(1)\",\n \"https://evil.com\",\n \"//evil.com\",\n \"///evil.com\",\n \"/\\\\evil.com\",\n \"/\\\\\\\\evil.com\",\n ])(\"strips href='%s'\", (href) => {\n const out = sanitizeRichText(`<a href=\"${href}\">x</a>`);\n const root = parseDom(out);\n expect(dangerousHrefsIn(root)).toEqual([]);\n root.querySelectorAll<HTMLAnchorElement>(\"a[href]\").forEach((a) => {\n expect(a.getAttribute(\"href\")).toMatch(/^\\/[^/\\\\]/);\n });\n });\n });\n\n describe(\"disallowed-but-tempting tags\", () => {\n it.each([\n \"<iframe src=javascript:alert(1)></iframe>\",\n \"<object data=javascript:alert(1)></object>\",\n \"<embed src=javascript:alert(1)></embed>\",\n \"<svg><script>alert(1)</script></svg>\",\n \"<svg onload=alert(1)></svg>\",\n \"<details ontoggle=alert(1) open>x</details>\",\n \"<marquee onstart=alert(1)>x</marquee>\",\n \"<video><source onerror=alert(1)></video>\",\n \"<form><input type=image src=x onerror=alert(1)></form>\",\n \"<isindex action=javascript:alert(1) type=image>\",\n \"<base href=javascript:alert(1)//>\",\n \"<meta http-equiv=refresh content=0;url=javascript:alert(1)>\",\n ])(\"strips '%s'\", (payload) => {\n const out = sanitizeRichText(payload + \"ok\");\n const root = parseDom(out);\n expect(scriptTagsIn(root)).toHaveLength(0);\n expect(eventHandlerAttrsOf(root)).toEqual([]);\n expect(dangerousHrefsIn(root)).toEqual([]);\n [\n \"iframe\",\n \"object\",\n \"embed\",\n \"svg\",\n \"details\",\n \"marquee\",\n \"video\",\n \"source\",\n \"form\",\n \"input\",\n \"isindex\",\n \"base\",\n \"meta\",\n ].forEach((t) => expect(root.querySelector(t)).toBeNull());\n });\n });\n});\n\ndescribe(\"sanitizeMarketingHtml\", () => {\n describe(\"baseline allowlist behaviour\", () => {\n it(\"keeps headings, lists, code, blockquote\", () => {\n const input =\n \"<h2>Title</h2><ul><li>One</li></ul><pre><code>code</code></pre><blockquote>quote</blockquote>\";\n expect(sanitizeMarketingHtml(input)).toBe(input);\n });\n\n it(\"allows external https links\", () => {\n const out = sanitizeMarketingHtml('<a href=\"https://ably.com\">go</a>');\n expect(out).toContain('href=\"https://ably.com\"');\n });\n\n it(\"allows http and relative URIs on img src\", () => {\n const out = sanitizeMarketingHtml(\n '<img src=\"https://ably.com/x.png\" alt=\"x\">',\n );\n expect(out).toContain('src=\"https://ably.com/x.png\"');\n });\n\n it(\"preserves figure/figcaption around images (Ghost-style)\", () => {\n const input =\n '<figure><img src=\"/x.png\" alt=\"x\"><figcaption>caption</figcaption></figure>';\n expect(sanitizeMarketingHtml(input)).toBe(input);\n });\n });\n\n describe(\"script and embed stripping\", () => {\n it.each([\n \"<script>alert(1)</script>\",\n '<script src=\"https://js.hscta.net/cta/current.js\"></script>',\n '<script type=\"application/ld+json\">{\"x\":1}</script>',\n \"<iframe src=https://youtube.com></iframe>\",\n \"<iframe src=javascript:alert(1)></iframe>\",\n '<object data=\"https://evil.com\"></object>',\n \"<embed src=javascript:alert(1)>\",\n \"<form action=javascript:alert(1)><input></form>\",\n \"<svg><script>alert(1)</script></svg>\",\n \"<svg onload=alert(1)></svg>\",\n \"<math><mtext><img src=x onerror=alert(1)></mtext></math>\",\n ])(\"strips '%s' while keeping trailing copy\", (payload) => {\n const out = sanitizeMarketingHtml(payload + \"copy\");\n const root = parseDom(out);\n expect(scriptTagsIn(root)).toHaveLength(0);\n expect(eventHandlerAttrsOf(root)).toEqual([]);\n expect(dangerousHrefsIn(root)).toEqual([]);\n [\n \"iframe\",\n \"object\",\n \"embed\",\n \"form\",\n \"input\",\n \"svg\",\n \"script\",\n \"math\",\n ].forEach((t) => expect(root.querySelector(t)).toBeNull());\n expect(out).toContain(\"copy\");\n });\n });\n\n describe(\"URL-protocol bypasses on allowed-tag attributes\", () => {\n it.each([\n \"javascript:alert(1)\",\n \"JaVaScRiPt:alert(1)\",\n \"java\\tscript:alert(1)\",\n \"java\\nscript:alert(1)\",\n \"javascript:alert(1)\",\n \"javascript:alert(1)\",\n \"javascript:alert(1)\",\n \"data:text/html,<script>alert(1)</script>\",\n \"vbscript:msgbox(1)\",\n \"file:///etc/passwd\",\n \"blob:https://example.com/abc\",\n ])(\"strips href='%s' on <a>\", (href) => {\n const out = sanitizeMarketingHtml(`<a href=\"${href}\">x</a>`);\n expect(dangerousHrefsIn(parseDom(out))).toEqual([]);\n });\n\n it.each([\n \"javascript:alert(1)\",\n \"data:image/svg+xml,<svg onload=alert(1)>\",\n \"vbscript:msgbox(1)\",\n ])(\"strips img src='%s'\", (src) => {\n const out = sanitizeMarketingHtml(`<img src=\"${src}\" alt=\"x\">`);\n expect(dangerousHrefsIn(parseDom(out))).toEqual([]);\n });\n });\n\n describe(\"authority-delimiter / protocol-relative bypasses\", () => {\n // Same corpus as the inline/rich-text wrappers - the marketing wrapper\n // earlier permitted `//evil.com` because the URI regex was unanchored\n // past the leading slash. Symmetric coverage protects against regression.\n it.each([\n \"//evil.com\",\n \"///evil.com\",\n \"/\\\\evil.com\",\n \"/\\\\\\\\evil.com\",\n \"/\\\\/evil.com\",\n \"/%5cevil.com\",\n ])(\"strips href='%s' on <a>\", (href) => {\n const out = sanitizeMarketingHtml(`<a href=\"${href}\">click</a>`);\n const root = parseDom(out);\n root.querySelectorAll<HTMLAnchorElement>(\"a[href]\").forEach((a) => {\n const raw = a.getAttribute(\"href\") ?? \"\";\n // A surviving href must be either a full http(s):// URL or a\n // single-slash same-origin path. No protocol-relative variants.\n expect(raw).toMatch(/^(https?:\\/\\/|\\/[^/\\\\])/);\n });\n });\n\n it.each([\"//evil.com/x.png\", \"/\\\\evil.com/x.png\", \"/\\\\\\\\evil.com/x.png\"])(\n \"strips img src='%s'\",\n (src) => {\n const out = sanitizeMarketingHtml(`<img src=\"${src}\" alt=\"x\">`);\n const root = parseDom(out);\n root.querySelectorAll<HTMLImageElement>(\"img[src]\").forEach((img) => {\n expect(img.getAttribute(\"src\")).toMatch(/^(https?:\\/\\/|\\/[^/\\\\])/);\n });\n },\n );\n });\n\n describe(\"style-based payloads\", () => {\n it(\"strips style attribute entirely (we do not allow it)\", () => {\n const out = sanitizeMarketingHtml(\n '<p style=\"background:url(javascript:alert(1))\">x</p>',\n );\n expect(out).not.toContain(\"style\");\n expect(out).not.toContain(\"javascript:\");\n });\n\n it(\"strips inline style with expression()\", () => {\n const out = sanitizeMarketingHtml(\n '<p style=\"width:expression(alert(1))\">x</p>',\n );\n expect(out).not.toContain(\"style\");\n expect(out).not.toContain(\"expression\");\n });\n });\n\n describe(\"event-handler corpus on allowed tags\", () => {\n it.each([\n '<p onclick=\"alert(1)\">x</p>',\n '<a href=\"/x\" onmouseover=\"alert(1)\">l</a>',\n '<img src=\"/x.png\" onerror=\"alert(1)\" alt=\"x\">',\n '<h2 onmouseenter=\"alert(1)\">t</h2>',\n '<table onclick=\"alert(1)\"><tbody><tr><td>x</td></tr></tbody></table>',\n ])(\"strips event handlers in '%s'\", (payload) => {\n const out = sanitizeMarketingHtml(payload);\n expect(eventHandlerAttrsOf(parseDom(out))).toEqual([]);\n });\n });\n\n describe(\"mXSS / parser-confusion vectors\", () => {\n it(\"strips noscript-wrapped img injection\", () => {\n const payload =\n '<noscript><p title=\"</noscript><img src=x onerror=alert(1)>\">';\n const out = sanitizeMarketingHtml(payload);\n const root = parseDom(out);\n expect(eventHandlerAttrsOf(root)).toEqual([]);\n const img = root.querySelector(\"img\");\n // If an <img> survives (it's allowed in marketing) it must not carry onerror\n if (img) {\n expect(img.getAttribute(\"onerror\")).toBeNull();\n expect(img.getAttribute(\"src\")).not.toBe(\"x\"); // src=x with no scheme is fine, but the onerror must be gone\n }\n });\n\n it(\"strips xlink:href javascript on svg use\", () => {\n const out = sanitizeMarketingHtml(\n '<svg><use xlink:href=\"javascript:alert(1)\"></use></svg>',\n );\n const root = parseDom(out);\n expect(root.querySelector(\"svg\")).toBeNull();\n expect(dangerousHrefsIn(root)).toEqual([]);\n });\n\n it(\"strips title-attribute escape inside an allowed tag\", () => {\n const payload =\n '<a href=\"/x\" title=\\'\"><script>alert(1)</script><p \\'>link</a>';\n const out = sanitizeMarketingHtml(payload);\n expect(scriptTagsIn(parseDom(out))).toHaveLength(0);\n });\n });\n});\n"],"names":["describe","expect","it","sanitizeInlineMarkup","sanitizeMarketingHtml","sanitizeRichText","parseDom","html","wrapper","document","createElement","innerHTML","eventHandlerAttrsOf","root","offenders","querySelectorAll","forEach","el","attr","Array","from","attributes","name","toLowerCase","startsWith","push","tagName","dangerousProtocols","dangerousHrefsIn","test","getAttribute","scriptTagsIn","toBe","out","toContain","undefined","each","payload","toHaveLength","toEqual","href","a","raw","toMatch","querySelector","toBeNull","input","t","src","img","not"],"mappings":"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"}
|