From bf4e0fc321cc70e6ac3a0a4a444fcde24f1207f0 Mon Sep 17 00:00:00 2001 From: Bingbing Date: Fri, 26 Dec 2025 16:10:38 +0800 Subject: [PATCH] feat: integrate customer-io in-app messages (#9510) --- package-lock.json | 139 ++++++++++++++++++ packages/insomnia/config/config.json | 10 ++ packages/insomnia/package.json | 3 +- packages/insomnia/src/common/constants.ts | 4 + packages/insomnia/src/entry.client.tsx | 33 +++++ packages/insomnia/src/main/api.protocol.ts | 7 + .../insomnia/src/ui/containers/app-hooks.tsx | 2 + packages/insomnia/src/ui/hooks/use-cio.tsx | 87 +++++++++++ 8 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 packages/insomnia/src/ui/hooks/use-cio.tsx diff --git a/package-lock.json b/package-lock.json index cf6e7b97b7..b8f833ea82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1901,6 +1901,57 @@ "node": ">=18" } }, + "node_modules/@customerio/cdp-analytics-browser": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@customerio/cdp-analytics-browser/-/cdp-analytics-browser-0.3.9.tgz", + "integrity": "sha512-Hx2VwlqXXDejM0kVg1TJYp5nWuPC9yYflH0t0UJE/J7jEtDiaOSPdEkbHk39XmkVxkaKAgxAX5SdtuUUi5C61w==", + "license": "MIT", + "dependencies": { + "@customerio/cdp-analytics-core": "0.3.9", + "@lukeed/uuid": "^2.0.0", + "@segment/analytics.js-video-plugins": "^0.2.1", + "@segment/facade": "^3.4.9", + "customerio-gist-web": "3.18.0", + "dset": "^3.1.2", + "js-cookie": "3.0.1", + "node-fetch": "^2.6.7", + "spark-md5": "^3.0.1", + "tslib": "^2.4.1", + "unfetch": "^4.1.0" + } + }, + "node_modules/@customerio/cdp-analytics-browser/node_modules/js-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", + "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@customerio/cdp-analytics-browser/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@customerio/cdp-analytics-core": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@customerio/cdp-analytics-core/-/cdp-analytics-core-0.3.9.tgz", + "integrity": "sha512-AjMB48tTu8JN4NAgmbOoGAS1veE7AvLUyZD+qKXZLD/muWy6Vou4MEfXLVzhQP/cIbf3VBPGUxMnugjwZFc6Sw==", + "license": "MIT", + "dependencies": { + "@lukeed/uuid": "^2.0.0", + "dset": "^3.1.2", + "tslib": "^2.4.1" + } + }, + "node_modules/@customerio/cdp-analytics-core/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@dependents/detective-less": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@dependents/detective-less/-/detective-less-5.0.1.tgz", @@ -8617,6 +8668,48 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/@segment/analytics.js-video-plugins": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@segment/analytics.js-video-plugins/-/analytics.js-video-plugins-0.2.1.tgz", + "integrity": "sha512-lZwCyEXT4aaHBLNK433okEKdxGAuyrVmop4BpQqQSJuRz0DglPZgd9B/XjiiWs1UyOankg2aNYMN3VcS8t4eSQ==", + "license": "ISC", + "dependencies": { + "unfetch": "^3.1.1" + } + }, + "node_modules/@segment/analytics.js-video-plugins/node_modules/unfetch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-3.1.2.tgz", + "integrity": "sha512-L0qrK7ZeAudGiKYw6nzFjnJ2D5WHblUBwmHIqtPS6oKUd+Hcpk7/hKsSmcHsTlpd1TbTNsiRBUKRq3bHLNIqIw==", + "license": "MIT" + }, + "node_modules/@segment/facade": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/@segment/facade/-/facade-3.4.10.tgz", + "integrity": "sha512-xVQBbB/lNvk/u8+ey0kC/+g8pT3l0gCT8O2y9Z+StMMn3KAFAQ9w8xfgef67tJybktOKKU7pQGRPolRM1i1pdA==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@segment/isodate-traverse": "^1.1.1", + "inherits": "^2.0.4", + "new-date": "^1.0.3", + "obj-case": "0.2.1" + } + }, + "node_modules/@segment/isodate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@segment/isodate/-/isodate-1.0.3.tgz", + "integrity": "sha512-BtanDuvJqnACFkeeYje7pWULVv8RgZaqKHWwGFnL/g/TH/CcZjkIVTfGDp/MAxmilYHUkrX70SqwnYSTNEaN7A==", + "license": "SEE LICENSE IN LICENSE" + }, + "node_modules/@segment/isodate-traverse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@segment/isodate-traverse/-/isodate-traverse-1.1.1.tgz", + "integrity": "sha512-+G6e1SgAUkcq0EDMi+SRLfT48TNlLPF3QnSgFGVs0V9F3o3fq/woQ2rHFlW20W0yy5NnCUH0QGU3Am2rZy/E3w==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@segment/isodate": "^1.0.3" + } + }, "node_modules/@sentry-internal/browser-utils": { "version": "9.11.0", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.11.0.tgz", @@ -14826,6 +14919,24 @@ "integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==", "license": "MIT" }, + "node_modules/customerio-gist-web": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/customerio-gist-web/-/customerio-gist-web-3.18.0.tgz", + "integrity": "sha512-3m48BGbiWxxoAYgHYPTBDwpOJ7q2byFEL5lhFfy5PjMujkz4Huqhzd/0lZxDNLEPALquX/HwOCxiWcvk+J0mvw==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "uuid": "^8.3.2" + } + }, + "node_modules/customerio-gist-web/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -23284,6 +23395,15 @@ "node": ">= 0.6" } }, + "node_modules/new-date": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/new-date/-/new-date-1.0.3.tgz", + "integrity": "sha512-0fsVvQPbo2I18DT2zVHpezmeeNYV2JaJSrseiHLc17GNOxJzUdx5mvSigPu8LtIfZSij5i1wXnXFspEs2CD6hA==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@segment/isodate": "1.0.3" + } + }, "node_modules/nimma": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.3.tgz", @@ -24528,6 +24648,12 @@ "integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==", "license": "MIT" }, + "node_modules/obj-case": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/obj-case/-/obj-case-0.2.1.tgz", + "integrity": "sha512-PquYBBTy+Y6Ob/O2574XHhDtHJlV1cJHMCgW+rDRc9J5hhmRelJB3k5dTK/3cVmFVtzvAKuENeuLpoyTzMzkOg==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -27989,6 +28115,12 @@ "deprecated": "Please use @jridgewell/sourcemap-codec instead", "license": "MIT" }, + "node_modules/spark-md5": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/spark-md5/-/spark-md5-3.0.2.tgz", + "integrity": "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==", + "license": "(WTFPL OR MIT)" + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -29601,6 +29733,12 @@ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "license": "MIT" }, + "node_modules/unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", + "license": "MIT" + }, "node_modules/unique-filename": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", @@ -30902,6 +31040,7 @@ "@bufbuild/protobuf": "^1.10.0", "@connectrpc/connect": "^1.6.1", "@connectrpc/connect-node": "^1.6.1", + "@customerio/cdp-analytics-browser": "^0.3.9", "@faker-js/faker": "9.7.0", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", diff --git a/packages/insomnia/config/config.json b/packages/insomnia/config/config.json index ac50250e5b..17db7bfea7 100644 --- a/packages/insomnia/config/config.json +++ b/packages/insomnia/config/config.json @@ -14,6 +14,16 @@ "development": "rTOCSvGV23cHGJyb3HI9EUQDNA6ar7ay", "production": "4l7QUfACrIcqvC913hiIwAA2BDYP2OJ1" }, + "cio": { + "development": { + "writeKey": "d819a0f74f1ba47aac56", + "siteId": "6a3e5585a91dc95f33d5" + }, + "production": { + "writeKey": "70d3f482cef87818efce", + "siteId": "4d5c703f3edbc5155bac" + } + }, "bundlePlugins": [ { "name": "@kong/insomnia-plugin-external-vault" diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index 659d80d821..233ec602d4 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -43,13 +43,13 @@ "@bufbuild/protobuf": "^1.10.0", "@connectrpc/connect": "^1.6.1", "@connectrpc/connect-node": "^1.6.1", + "@customerio/cdp-analytics-browser": "^0.3.9", "@faker-js/faker": "9.7.0", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^3.0.2", - "blakejs": "^1.2.1", "@getinsomnia/node-libcurl": "3.1.0", "@getinsomnia/srp-js": "1.0.0-alpha.1", "@grpc/grpc-js": "^1.13.3", @@ -72,6 +72,7 @@ "@xmldom/xmldom": "^0.9.8", "apiconnect-wsdl": "2.0.36", "aws4": "^1.13.2", + "blakejs": "^1.2.1", "buffer": "^6.0.3", "chai": "^4.5.0", "chai-json-schema": "1.5.1", diff --git a/packages/insomnia/src/common/constants.ts b/packages/insomnia/src/common/constants.ts index 83096795b9..da653856c0 100644 --- a/packages/insomnia/src/common/constants.ts +++ b/packages/insomnia/src/common/constants.ts @@ -38,6 +38,10 @@ export const isDevelopment = () => getAppEnvironment() === 'development'; export const getSegmentWriteKey = () => appConfig.segmentWriteKeys[isDevelopment() || env.PLAYWRIGHT ? 'development' : 'production']; export const getSentryDsn = () => appConfig.sentryDsn; +export const getCioWriteKey = () => + appConfig.cio[isDevelopment() || env.PLAYWRIGHT ? 'development' : 'production'].writeKey; +export const getCioSiteId = () => + appConfig.cio[isDevelopment() || env.PLAYWRIGHT ? 'development' : 'production'].siteId; export const getAppBuildDate = () => new Date(process.env.BUILD_DATE ?? '').toLocaleDateString(); export const getBrowserUserAgent = () => diff --git a/packages/insomnia/src/entry.client.tsx b/packages/insomnia/src/entry.client.tsx index 5c84d66a51..ef6bcf74df 100644 --- a/packages/insomnia/src/entry.client.tsx +++ b/packages/insomnia/src/entry.client.tsx @@ -56,6 +56,39 @@ try { console.log('[onboarding] Failed to parse session data', e); } +// Workaround for iframe redirect issue caused by api.protocol.ts +// Problem: The https protocol handler (registerInsomniaProtocols) intercepts all https requests +// to solve CORS issues. However, when an iframe redirects from https://renderer.gist.build to +// https://code.gist.build, the protocol handler auto-follows the redirect but the iframe's +// location doesn't update. This causes the Customer.io SDK to fail origin validation. +// +// Solution: Intercept postMessage events from renderer.gist.build in the capture phase, +// stop propagation, and re-dispatch with origin changed to code.gist.build. This makes +// the SDK think the message came from the expected redirected URL. +window.addEventListener( + 'message', + (event: MessageEvent) => { + // If origin is renderer.gist.build (original URL), stop propagation and dispatch a new event + if (event.origin === 'https://renderer.gist.build') { + // Stop the original event from reaching other listeners + event.stopImmediatePropagation(); + + // Create and dispatch a new MessageEvent with modified origin + // Note: 'ports' property is read-only and cannot be set, but the SDK doesn't use it + const newEvent = new MessageEvent('message', { + data: event.data, + origin: 'https://code.gist.build', + lastEventId: event.lastEventId, + source: event.source, + }); + + window.dispatchEvent(newEvent); + return; + } + }, + true, // Use capture phase to intercept before other listeners +); + // Check if there is a Session provided by an env variable and use this const insomniaSession = getInsomniaSession(); const insomniaVaultKey = getInsomniaVaultKey() || ''; diff --git a/packages/insomnia/src/main/api.protocol.ts b/packages/insomnia/src/main/api.protocol.ts index cae0e3499c..96ad3609ac 100644 --- a/packages/insomnia/src/main/api.protocol.ts +++ b/packages/insomnia/src/main/api.protocol.ts @@ -188,6 +188,13 @@ export async function registerInsomniaProtocols() { return await net.fetch(`file://${filePath}`, { bypassCustomProtocolHandlers: true }); } + // Allow Google Fonts to bypass the custom https protocol handler. + // Some embedded UIs (including the Customer.io in-app messaging/marketing SDK) load fonts from Google fonts. + // When those requests are routed through our custom https handler they fail due to unknown issues. + if (url.hostname === 'fonts.googleapis.com' || url.hostname === 'fonts.gstatic.com') { + return net.fetch(request.url, { bypassCustomProtocolHandlers: true }); + } + return net.fetch(request, { bypassCustomProtocolHandlers: true }); }); } diff --git a/packages/insomnia/src/ui/containers/app-hooks.tsx b/packages/insomnia/src/ui/containers/app-hooks.tsx index b91f9f0565..b0f2377835 100644 --- a/packages/insomnia/src/ui/containers/app-hooks.tsx +++ b/packages/insomnia/src/ui/containers/app-hooks.tsx @@ -1,5 +1,6 @@ import { type FC, useEffect } from 'react'; +import { useCio } from '../hooks/use-cio'; import { useGlobalKeyboardShortcuts } from '../hooks/use-global-keyboard-shortcuts'; import { useSettingsSideEffects } from '../hooks/use-settings-side-effects'; import { useThemeChange } from '../hooks/use-theme-change'; @@ -8,6 +9,7 @@ export const AppHooks: FC = () => { useSettingsSideEffects(); useGlobalKeyboardShortcuts(); useThemeChange(); + useCio(); // Used for detecting if we just updated Insomnia and app --args or insomnia:// and useEffect(() => { setTimeout(() => window.main.halfSecondAfterAppStart(), 500); diff --git a/packages/insomnia/src/ui/hooks/use-cio.tsx b/packages/insomnia/src/ui/hooks/use-cio.tsx new file mode 100644 index 0000000000..9ba825eb8b --- /dev/null +++ b/packages/insomnia/src/ui/hooks/use-cio.tsx @@ -0,0 +1,87 @@ +import type { AnalyticsBrowser } from '@customerio/cdp-analytics-browser'; +import { useEffect, useRef } from 'react'; + +import { getCioSiteId, getCioWriteKey } from '~/common/constants'; +import { useRootLoaderData } from '~/root'; + +// Global singleton +let globalAnalyticsInstance: AnalyticsBrowser | null = null; +let isInitializing = false; +let pendingIdentify: (() => void) | null = null; + +export const useCio = () => { + const { userSession } = useRootLoaderData()!; + const lastIdentifiedUser = useRef(null); + + // Initialize SDK once + useEffect(() => { + if (globalAnalyticsInstance || isInitializing) { + return; + } + + isInitializing = true; + console.log('[CIO] Initializing SDK...'); + + import('@customerio/cdp-analytics-browser') + .then(({ AnalyticsBrowser }) => { + globalAnalyticsInstance = AnalyticsBrowser.load( + { + cdnURL: 'https://cdp-eu.customer.io', + writeKey: getCioWriteKey(), + }, + { + integrations: { + 'Customer.io In-App Plugin': { + siteId: getCioSiteId(), + // _logging: true, + events: { + handleEvent(e: Event) { + console.log('[CIO] Event', e.type, (e as CustomEvent).detail); + }, + } as any, // The interface of the events is incorrect, see: https://docs.customer.io/integrations/data-in/connections/javascript/js-source/#import-the-javascript-client + }, + }, + }, + ); + console.log('[CIO] SDK initialized successfully'); + + // Execute pending identify + if (pendingIdentify) { + pendingIdentify(); + pendingIdentify = null; + } + }) + .catch(err => { + console.error('[CIO] Failed to load SDK:', err); + }) + .finally(() => { + isInitializing = false; + }); + }, []); + + // Handle user identification + useEffect(() => { + const currentUserId = userSession?.accountId; + if (!currentUserId || currentUserId === lastIdentifiedUser.current) { + return; + } + + const identifyCall = () => { + globalAnalyticsInstance?.identify(currentUserId, { + email: userSession.email, + first_name: userSession.firstName, + last_name: userSession.lastName, + }); + globalAnalyticsInstance?.page(); + lastIdentifiedUser.current = currentUserId; + }; + + if (globalAnalyticsInstance) { + identifyCall(); + } else { + pendingIdentify = identifyCall; + } + }, [userSession?.accountId, userSession?.email, userSession?.firstName, userSession?.lastName]); + + return globalAnalyticsInstance; +};