feat: integrate customer-io in-app messages (#9510)

This commit is contained in:
Bingbing
2025-12-26 16:10:38 +08:00
committed by GitHub
parent 5aad514412
commit bf4e0fc321
8 changed files with 284 additions and 1 deletions

139
package-lock.json generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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",

View File

@@ -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 = () =>

View File

@@ -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() || '';

View File

@@ -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 });
});
}

View File

@@ -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);

View File

@@ -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<string | null>(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;
};