diff --git a/.changeset/early-comics-add.md b/.changeset/early-comics-add.md new file mode 100644 index 000000000..a9c072577 --- /dev/null +++ b/.changeset/early-comics-add.md @@ -0,0 +1,5 @@ +--- +'@segment/analytics-signals-runtime': minor +--- + +Update ProcessSignal type for experimental "constants" object diff --git a/.changeset/three-oranges-work.md b/.changeset/three-oranges-work.md new file mode 100644 index 000000000..b179ebbd8 --- /dev/null +++ b/.changeset/three-oranges-work.md @@ -0,0 +1,5 @@ +--- +'@segment/analytics-signals': minor +--- + +Update iframe sandbox so CSP only requires :blob permission diff --git a/packages/signals/signals-example/src/lib/analytics.ts b/packages/signals/signals-example/src/lib/analytics.ts index bd591b41b..d12c79d1a 100644 --- a/packages/signals/signals-example/src/lib/analytics.ts +++ b/packages/signals/signals-example/src/lib/analytics.ts @@ -2,7 +2,11 @@ // You only want to instantiate SignalsPlugin in a browser context, otherwise you'll get an error. import { AnalyticsBrowser } from '@segment/analytics-next' -import { SignalsPlugin, ProcessSignal } from '@segment/analytics-signals' +import { + SignalsPlugin, + SignalsPluginSettingsConfig, + ProcessSignal, +} from '@segment/analytics-signals' export const analytics = new AnalyticsBrowser() if (!process.env.WRITEKEY) { @@ -29,11 +33,20 @@ const processSignalExample: ProcessSignal = ( } } +const getQueryParams = () => { + const params = new URLSearchParams() + const sandboxStrategy = params.get('sandboxStrategy') + return { + sandboxStrategy: + sandboxStrategy as SignalsPluginSettingsConfig['sandboxStrategy'], + } +} const isStage = process.env.STAGE === 'true' +const queryParams = getQueryParams() const signalsPlugin = new SignalsPlugin({ ...(isStage ? { apiHost: 'signals.segment.build/v1' } : {}), - sandboxStrategy: 'global', + sandboxStrategy: queryParams.sandboxStrategy ?? 'iframe', // processSignal: processSignalExample, }) diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-select.test.ts b/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-select.test.ts index 03b2ed2d7..b9d63a922 100644 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-select.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-select.test.ts @@ -3,7 +3,7 @@ import { waitForCondition } from '../../helpers/playwright-utils' import { IndexPage } from './index-page' import type { SegmentEvent } from '@segment/analytics-next' -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` +const basicEdgeFn = `function processSignal(signal) {}` test('Collecting signals whenever a user selects an item', async ({ page }) => { const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-textfield.test.ts b/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-textfield.test.ts index b7199fbeb..32e1319cc 100644 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-textfield.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-textfield.test.ts @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test' import { waitForCondition } from '../../helpers/playwright-utils' import { IndexPage } from './index-page' -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` +const basicEdgeFn = `function processSignal(signal) {}` test('Collecting signals whenever a user enters text input and focuses out', async ({ page, diff --git a/packages/signals/signals-integration-tests/src/tests/performance/memory-leak.test.ts b/packages/signals/signals-integration-tests/src/tests/performance/memory-leak.test.ts index 2d1a7e891..9c22ba9b8 100644 --- a/packages/signals/signals-integration-tests/src/tests/performance/memory-leak.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/performance/memory-leak.test.ts @@ -16,7 +16,7 @@ declare global { const basicEdgeFn = ` // this is a process signal function - globalThis.processSignal = (signal) => { + function processSignal(signal) { if (signal.type === 'interaction') { const eventName = signal.data.eventType + ' ' + '[' + signal.type + ']' analytics.track(eventName, signal.data) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts index 58b3cafdf..f3de2f187 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts @@ -6,7 +6,6 @@ import { SegmentEvent } from '@segment/analytics-next' /** * This test ensures that */ -const indexPage = new IndexPage() const normalizeSnapshotEvent = (el: SegmentEvent) => { return { @@ -35,9 +34,10 @@ const snapshot = ( ).map(normalizeSnapshotEvent) test('Segment events', async ({ page }) => { + const indexPage = new IndexPage() const basicEdgeFn = ` // this is a process signal function - globalThis.processSignal = (signal) => { + function processSignal(signal) { if (signal.type === 'interaction' && signal.data.eventType === 'click') { analytics.identify('john', { found: true }) analytics.group('foo', { hello: 'world' }) @@ -64,8 +64,9 @@ test('Segment events', async ({ page }) => { test('Should dispatch events from signals that occurred before analytics was instantiated', async ({ page, }) => { + const indexPage = new IndexPage() const edgeFn = ` - globalThis.processSignal = (signal) => { + function processSignal(signal) { if (signal.type === 'navigation' && signal.data.action === 'pageLoad') { analytics.page('dispatched from signals - navigation') } @@ -76,23 +77,25 @@ test('Should dispatch events from signals that occurred before analytics was ins await indexPage.load(page, edgeFn) const flush = Promise.all([ + indexPage.addUserDefinedSignal(), indexPage.waitForSignalsApiFlush(), indexPage.waitForTrackingApiFlush(), ]) - // add a user defined signal before analytics is instantiated - void indexPage.addUserDefinedSignal() await flush const trackingApiReqs = indexPage.trackingAPI.getEvents() expect(trackingApiReqs).toHaveLength(2) const pageEvents = trackingApiReqs.find((el) => el.type === 'page')! + expect(pageEvents).toBeTruthy() expect(pageEvents.name).toEqual('dispatched from signals - navigation') const userDefinedEvents = trackingApiReqs.find((el) => el.type === 'track')! - expect(userDefinedEvents).toBeTruthy() + if (!userDefinedEvents) { + console.warn('invariant', trackingApiReqs) + } expect(userDefinedEvents.event).toEqual( 'dispatched from signals - userDefined' ) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts index ff618682e..93d8b4d6f 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts @@ -4,7 +4,7 @@ import { IndexPage } from './index-page' const basicEdgeFn = ` // this is a process signal function - globalThis.processSignal = (signal) => { + function processSignal(signal) { if (signal.type === 'interaction') { const eventName = signal.data.eventType + ' ' + '[' + signal.type + ']' analytics.track(eventName, signal.data) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/button-click-complex.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/button-click-complex.test.ts index 864dec3b1..471207bd8 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/button-click-complex.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/button-click-complex.test.ts @@ -1,7 +1,7 @@ import { test, expect } from '@playwright/test' import { IndexPage } from './index-page' -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` +const basicEdgeFn = `function processSignal(signal) {}` let indexPage: IndexPage test.beforeEach(async ({ page }) => { indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts index df297f8b6..c123cd730 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test' import { waitForCondition } from '../../helpers/playwright-utils' import { IndexPage } from './index-page' -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` +const basicEdgeFn = `function processSignal(signal) {}` test('Collecting signals whenever a user enters text input', async ({ page, diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/middleware.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/middleware.test.ts index 244a787d7..10dc88913 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/middleware.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/middleware.test.ts @@ -1,7 +1,7 @@ import { test, expect } from '@playwright/test' import { IndexPage } from './index-page' -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` +const basicEdgeFn = `function processSignal(signal) {}` let indexPage: IndexPage diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-allow-list.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-allow-list.test.ts index 097813b6e..db8f9e9dd 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-allow-list.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-allow-list.test.ts @@ -1,7 +1,7 @@ import { test, expect } from '@playwright/test' import { IndexPage } from './index-page' -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` +const basicEdgeFn = `function processSignal(signal) {}` test('network signals allow and disallow list', async ({ page }) => { const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-fetch.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-fetch.test.ts index 6d92d67bc..bc8509034 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-fetch.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-fetch.test.ts @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test' import { commonSignalData } from '../../helpers/fixtures' import { IndexPage } from './index-page' -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` +const basicEdgeFn = `function processSignal(signal) {}` test.describe('network signals - fetch', () => { let indexPage: IndexPage diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-xhr.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-xhr.test.ts index 13729c583..301105019 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-xhr.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-xhr.test.ts @@ -1,7 +1,7 @@ import { test, expect } from '@playwright/test' import { IndexPage } from './index-page' -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` +const basicEdgeFn = `function processSignal(signal) {}` test.describe('network signals - XHR', () => { let indexPage: IndexPage diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/reset.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/reset.test.ts index 5c42651c1..d45bf3ae5 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/reset.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/reset.test.ts @@ -7,7 +7,7 @@ import { pTimeout } from '@segment/analytics-core' * If a signal is generated, the signal buffer should be reset * when the user clicks on the complex button. */ -const edgeFn = `globalThis.processSignal = (signal) => { +const edgeFn = `function processSignal(signal) { // create a custom signal to echo out the current signal buffer if (signal.type === 'userDefined') { analytics.track('current signal buffer', { signalBuffer: signals.signalBuffer }) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/runtime-constants.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/runtime-constants.test.ts index 644d18366..10c66f083 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/runtime-constants.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/runtime-constants.test.ts @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test' import { IndexPage } from './index-page' const basicEdgeFn = ` - globalThis.processSignal = (signal) => { + function processSignal(signal) { // test that constants are properly injected if (typeof EventType !== 'object') { throw new Error('EventType is missing?') diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-find.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-find.test.ts index d9099f337..5ab3cb27b 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-find.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-find.test.ts @@ -4,7 +4,7 @@ import { IndexPage } from './index-page' const indexPage = new IndexPage() test('should find the most recent signal', async ({ page }) => { - const basicEdgeFn = `globalThis.processSignal = (signal) => { + const basicEdgeFn = `function processSignal(signal) { if (signal.type === 'interaction' && signal.data.target.id === 'complex-button') { const mostRecentSignal = signals.find(signal, 'userDefined') if (mostRecentSignal.data.num === 2) { diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts index 25f25d4b3..d18c074be 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts @@ -4,7 +4,7 @@ import { waitForCondition } from '../../helpers/playwright-utils' const indexPage = new IndexPage() -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` +const basicEdgeFn = `function processSignal(signal) {}` test('debug ingestion disabled and sample rate 0 -> will not send the signal', async ({ page, diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts index 88e09b54d..030e339bc 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test' import { waitForCondition } from '../../helpers/playwright-utils' import { IndexPage } from './index-page' -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` +const basicEdgeFn = `function processSignal(signal) {}` test('redaction enabled -> will XXX the value of text input', async ({ page, diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/top-level-metadata.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/top-level-metadata.test.ts index 02f22caea..bb0a11e09 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/top-level-metadata.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/top-level-metadata.test.ts @@ -4,7 +4,7 @@ import { IndexPage } from './index-page' const basicEdgeFn = ` // this is a process signal function - globalThis.processSignal = (signal) => { + function processSignal(signal) { if (signal.type === 'interaction') { analytics.track('hello', { myAnonId: signal.anonymousId, myTimestamp: signal.timestamp }) } diff --git a/packages/signals/signals-runtime/package.json b/packages/signals/signals-runtime/package.json index 42f3d9dfd..86c26c84c 100644 --- a/packages/signals/signals-runtime/package.json +++ b/packages/signals/signals-runtime/package.json @@ -25,7 +25,6 @@ "build:esm": "yarn tsc -p tsconfig.build.json", "build:cjs": "yarn tsc -p tsconfig.build.json --outDir ./dist/cjs --module commonjs", "build:global": "node build-signals-runtime-global.js", - "assert-generated": "bash scripts/assert-generated.sh", "watch": "rm -rf dist/esm && yarn build:esm && yarn build:esm --watch", "watch:test": "yarn test --watch", "tsc": "yarn run -T tsc", diff --git a/packages/signals/signals-runtime/scripts/assert-generated.sh b/packages/signals/signals-runtime/scripts/assert-generated.sh deleted file mode 100644 index 66f9719ca..000000000 --- a/packages/signals/signals-runtime/scripts/assert-generated.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -# A CI script to ensure people remember to rebuild workerbox related files if workerbox changes - -yarn build:global - -# Check for changes in the workerbox directory -changed_files=$(git diff --name-only | grep 'generated') - -# Check for changes in the workerbox directory -if [ -n "$changed_files" ]; then - echo "Error: Changes detected. Please commit the changed files:" - echo "$changed_files" - exit 1 -else - echo "Files have not changed" - exit 0 -fi diff --git a/packages/signals/signals/.lintstagedrc.js b/packages/signals/signals/.lintstagedrc.js index 7fa7ceee1..8255ac3b5 100644 --- a/packages/signals/signals/.lintstagedrc.js +++ b/packages/signals/signals/.lintstagedrc.js @@ -1,5 +1,3 @@ -module.exports = { - ...require("@internal/config").lintStagedConfig, - 'src/lib/workerbox/*.{js,ts,html}': ['yarn workerbox'] -} +module.exports = require("@internal/config").lintStagedConfig + diff --git a/packages/signals/signals/package.json b/packages/signals/signals/package.json index aa402a9b2..e552a49a3 100644 --- a/packages/signals/signals/package.json +++ b/packages/signals/signals/package.json @@ -32,8 +32,6 @@ "build:esm": "yarn tsc -p tsconfig.build.json", "build:cjs": "yarn tsc -p tsconfig.build.json --outDir ./dist/cjs --module commonjs", "build:bundle": "NODE_ENV=production yarn run webpack", - "workerbox": "node scripts/build-workerbox.js", - "assert-generated": "sh scripts/assert-workerbox-built.sh", "watch": "rm -rf dist && yarn concurrently 'yarn build:bundle --watch' 'yarn build:esm --watch'", "version": "sh scripts/version.sh", "watch:test": "yarn test --watch", diff --git a/packages/signals/signals/scripts/assert-workerbox-built.sh b/packages/signals/signals/scripts/assert-workerbox-built.sh deleted file mode 100644 index d5550d409..000000000 --- a/packages/signals/signals/scripts/assert-workerbox-built.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -# A CI script to ensure people remember to rebuild workerbox related files if workerbox changes - -node scripts/build-workerbox.js -# Check for changes in the workerbox directory -changed_files=$(git diff --name-only | grep 'lib/workerbox') - -# Check for changes in the workerbox directory -if [ -n "$changed_files" ]; then - echo "Error: Changes detected in the workerbox directory. Please commit the changed files:" - echo "$changed_files" - exit 1 -else - echo "Files have not changed" - exit 0 -fi \ No newline at end of file diff --git a/packages/signals/signals/scripts/build-workerbox.js b/packages/signals/signals/scripts/build-workerbox.js deleted file mode 100644 index d1bc1da3b..000000000 --- a/packages/signals/signals/scripts/build-workerbox.js +++ /dev/null @@ -1,64 +0,0 @@ -const fs = require('fs') -const esbuild = require('esbuild') -const path = require('path') - -// Note: This was adopted from the https://github.com/markwylde/workerbox/blob/master/build.js -console.log('Building workerbox...') - -const DEBUG = process.env.DEBUG === 'true' -if (DEBUG) console.log('Minification off.') - -async function writeFileWithDirs(filePath, data) { - // Extract the directory path from the file path - const dir = path.dirname(filePath) - - // Ensure the directory exists - await fs.promises.mkdir(dir, { recursive: true }) - - // Write the file - await fs.promises.writeFile(filePath, data, 'utf8') -} - -async function build() { - console.log(new Date(), 'rebuilding...') - - // clean up dist folder - await fs.promises.rm('./src/lib/workerbox/dist', { - recursive: true, - force: true, - }) - - await esbuild.build({ - entryPoints: ['./src/lib/workerbox/worker.ts'], - bundle: true, - outfile: './src/lib/workerbox/dist/worker.js', - minify: !DEBUG, - }) - - const jsData = await fs.promises.readFile( - './src/lib/workerbox/dist/worker.js', - 'utf8' - ) - - const TEMPLATE_PLACEHOLDER = `{{WORKERSCRIPT}}` - const htmlData = ( - await fs.promises.readFile('./src/lib/workerbox/worker.html', 'utf8') - ).replace(TEMPLATE_PLACEHOLDER, jsData) - - await writeFileWithDirs('./src/lib/workerbox/dist/worker.html', htmlData) - await writeFileWithDirs( - './src/lib/workerbox/worker.generated.ts', - [ - '/* eslint-disable */', - `// built from /dist/worker.html`, - `export default atob('${Buffer.from(htmlData).toString('base64')}');`, - ].join('\n') - ) -} - -build() - .then(() => console.log('Build successful')) - .catch((err) => { - console.error(err) - process.exit(1) - }) diff --git a/packages/signals/signals/src/core/middleware/event-processor/index.ts b/packages/signals/signals/src/core/middleware/event-processor/index.ts index f51f52c73..822fea57c 100644 --- a/packages/signals/signals/src/core/middleware/event-processor/index.ts +++ b/packages/signals/signals/src/core/middleware/event-processor/index.ts @@ -6,10 +6,9 @@ import { SignalEventProcessor } from '../../processor/processor' import { normalizeEdgeFunctionURL, GlobalScopeSandbox, - WorkerSandbox, - IframeSandboxSettings, SignalSandbox, NoopSandbox, + IframeSandbox, } from '../../processor/sandbox' export class SignalsEventProcessorSubscriber implements SignalsSubscriber { @@ -36,11 +35,9 @@ export class SignalsEventProcessorSubscriber implements SignalsSubscriber { sandboxSettings.processSignal ) { logger.debug('Initializing sandbox: iframe') - sandbox = new WorkerSandbox( - new IframeSandboxSettings({ - processSignal: sandboxSettings.processSignal, - edgeFnDownloadURL: normalizedEdgeFunctionURL, - }) + sandbox = new IframeSandbox( + normalizedEdgeFunctionURL, + sandboxSettings.processSignal ) } else { logger.debug('Initializing sandbox: global scope') diff --git a/packages/signals/signals/src/core/processor/__tests__/sandbox-settings.test.ts b/packages/signals/signals/src/core/processor/__tests__/sandbox-settings.test.ts deleted file mode 100644 index f5c5e9c02..000000000 --- a/packages/signals/signals/src/core/processor/__tests__/sandbox-settings.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { IframeSandboxSettings, IframeSandboxSettingsConfig } from '../sandbox' - -describe(IframeSandboxSettings, () => { - const edgeFnResponseBody = `function processSignal() { console.log('hello world') }` - const baseSettings: IframeSandboxSettingsConfig = { - processSignal: undefined, - edgeFnDownloadURL: 'http://example.com/download', - edgeFnFetchClient: jest.fn().mockReturnValue( - Promise.resolve({ - text: () => edgeFnResponseBody, - }) - ), - } - test('initializes with provided settings', async () => { - const sandboxSettings = new IframeSandboxSettings({ ...baseSettings }) - expect(baseSettings.edgeFnFetchClient).toHaveBeenCalledWith( - baseSettings.edgeFnDownloadURL - ) - expect(await sandboxSettings.processSignal).toEqual(edgeFnResponseBody) - }) - - test('should call edgeFnDownloadURL', async () => { - const settings: IframeSandboxSettingsConfig = { - ...baseSettings, - processSignal: undefined, - edgeFnDownloadURL: 'https://foo.com/download', - } - new IframeSandboxSettings(settings) - expect(baseSettings.edgeFnFetchClient).toHaveBeenCalledWith( - 'https://foo.com/download' - ) - }) - - test('creates default processSignal when parameters are missing', async () => { - const consoleWarnSpy = jest - .spyOn(console, 'warn') - .mockImplementation(() => {}) - const settings: IframeSandboxSettingsConfig = { - ...baseSettings, - processSignal: undefined, - edgeFnDownloadURL: undefined, - } - const sandboxSettings = new IframeSandboxSettings(settings) - expect(await sandboxSettings.processSignal).toMatchInlineSnapshot( - `"globalThis.processSignal = function() {}"` - ) - expect(baseSettings.edgeFnFetchClient).not.toHaveBeenCalled() - expect(consoleWarnSpy).toHaveBeenCalledWith( - expect.stringContaining('processSignal') - ) - }) -}) diff --git a/packages/signals/signals/src/core/processor/polyfills.ts b/packages/signals/signals/src/core/processor/polyfills.ts deleted file mode 100644 index 4255c0a44..000000000 --- a/packages/signals/signals/src/core/processor/polyfills.ts +++ /dev/null @@ -1,16 +0,0 @@ -const globalThisPolyfill = `(function () { - // polyfill for globalThis - if (typeof globalThis === 'undefined') { - if (typeof self !== 'undefined') { - self.globalThis = self - } else if (typeof window !== 'undefined') { - window.globalThis = window - } else if (typeof global !== 'undefined') { - global.globalThis = global - } else { - throw new Error('Unable to locate global object') - } - } -})()` - -export const polyfills = [globalThisPolyfill].join('\n') diff --git a/packages/signals/signals/src/core/processor/sandbox.ts b/packages/signals/signals/src/core/processor/sandbox.ts index 78c9b3e3f..26315936c 100644 --- a/packages/signals/signals/src/core/processor/sandbox.ts +++ b/packages/signals/signals/src/core/processor/sandbox.ts @@ -1,5 +1,4 @@ import { logger } from '../../lib/logger' -import { createWorkerBox, WorkerBoxAPI } from '../../lib/workerbox' import { resolvers } from './arg-resolvers' import { AnalyticsRuntimePublicApi, ProcessSignal } from '../../types' import { replaceBaseUrl } from '../../lib/replace-base-url' @@ -9,7 +8,6 @@ import { WebSignalsRuntime, } from '@segment/analytics-signals-runtime' import { getRuntimeCode } from '@segment/analytics-signals-runtime' -import { polyfills } from './polyfills' import { loadScript } from '../../lib/load-script' export type MethodName = @@ -129,33 +127,6 @@ class AnalyticsRuntime implements AnalyticsRuntimePublicApi { } } -interface CodeSandbox { - run: (fn: string, scope: Record) => Promise - destroy: () => Promise -} - -class JavascriptSandbox implements CodeSandbox { - private workerbox: Promise - constructor() { - this.workerbox = createWorkerBox() - } - async run(fn: string, scope: Record) { - try { - const wb = await this.workerbox - await wb.run(fn, scope) - } catch (err) { - console.error('processSignal() error in sandbox', err, { - fn, - }) - } - } - - async destroy(): Promise { - const wb = await this.workerbox - await wb.destroy() - } -} - export const normalizeEdgeFunctionURL = ( functionHost: string | undefined, edgeFnDownloadURL: string | undefined @@ -180,43 +151,10 @@ export type IframeSandboxSettingsConfig = Pick< 'processSignal' | 'edgeFnFetchClient' | 'edgeFnDownloadURL' > -const consoleWarnProcessSignal = () => - console.warn( - 'processSignal is not defined - have you set up auto-instrumentation on app.segment.com?' - ) - -export class IframeSandboxSettings { - /** - * Should look like: - * ```js - * function processSignal(signal) { - * ... - * } - * ``` - */ - processSignal: Promise - constructor(settings: IframeSandboxSettingsConfig) { - const fetch = settings.edgeFnFetchClient ?? globalThis.fetch - - let processSignalNormalized = Promise.resolve( - `globalThis.processSignal = function() {}` - ) - - if (settings.processSignal) { - processSignalNormalized = Promise.resolve(settings.processSignal).then( - (str) => `globalThis.processSignal = ${str}` - ) - } else if (settings.edgeFnDownloadURL) { - processSignalNormalized = fetch(settings.edgeFnDownloadURL!).then((res) => - res.text() - ) - } else { - consoleWarnProcessSignal() - } +const PROCESS_SIGNAL_UNDEFINED = + 'processSignal is not defined - have you set up auto-instrumentation on app.segment.com?' - this.processSignal = processSignalNormalized - } -} +const consoleWarnProcessSignal = () => console.warn(PROCESS_SIGNAL_UNDEFINED) export interface SignalSandbox { execute( @@ -226,43 +164,6 @@ export interface SignalSandbox { destroy(): void | Promise } -export class WorkerSandbox implements SignalSandbox { - settings: IframeSandboxSettings - jsSandbox: CodeSandbox - - constructor(settings: IframeSandboxSettings) { - this.settings = settings - this.jsSandbox = new JavascriptSandbox() - } - - async execute( - signal: Signal, - signals: Signal[] - ): Promise { - const analytics = new AnalyticsRuntime() - const scope = { - analytics, - } - logger.debug('processing signal', { signal, scope, signals }) - const code = [ - polyfills, - await this.settings.processSignal, - getRuntimeCode(), - `signals.signalBuffer = ${JSON.stringify(signals)};`, - 'try { processSignal(' + - JSON.stringify(signal) + - ', { analytics, signals, SignalType, EventType, NavigationAction }); } catch(err) { console.error("Process signal failed.", err); }', - ].join('\n') - await this.jsSandbox.run(code, scope) - - const calls = analytics.getCalls() - return calls - } - destroy(): void { - void this.jsSandbox.destroy() - } -} - // ProcessSignal unfortunately uses globals. This should change. // For now, we are setting up the globals between each invocation const processWithGlobalScopeExecutionEnv = ( @@ -302,10 +203,7 @@ const processWithGlobalScopeExecutionEnv = ( // TODO: update processSignal generator to accept params like these for web (mobile currently uses globals for their architecture -- can be changed but hard). analytics: analytics, signals: signals, - // constants - EventType: WebRuntimeConstants.EventType, - NavigationAction: WebRuntimeConstants.NavigationAction, - SignalType: WebRuntimeConstants.SignalType, + constants: WebRuntimeConstants, }) } finally { // restore globals @@ -317,7 +215,7 @@ const processWithGlobalScopeExecutionEnv = ( } /** - * Sandbox that avoids CSP errors, but evaluates everything globally + * Sandbox that invokes processSignal in the global scope (avoids all CSP errors) */ interface GlobalScopeSandboxSettings { edgeFnDownloadURL: string @@ -343,3 +241,193 @@ export class NoopSandbox implements SignalSandbox { } destroy(): void {} } + +/** + * Sandbox that executes code in an iframe. + * Pros: + * - More secure + * Cons: + * - Can trigger CSP errors unless :blob directive is present + */ +export class IframeSandbox implements SignalSandbox { + private iframe: HTMLIFrameElement + private iframeReady: Promise + edgeFnUrl: string + + constructor(edgeFnUrl: string, processSignalFn?: string) { + logger.debug('Initializing iframe sandbox') + this.edgeFnUrl = edgeFnUrl + const innerHTML = this.buildIframeInnerHTMLWithScriptTag( + Boolean(processSignalFn) + ) + const jsScriptContent = this.getIframeScriptJSScriptContent(processSignalFn) + const iframe = document.createElement('iframe') + iframe.id = 'segment-signals-sandbox' + iframe.style.display = 'none' + iframe.src = 'about:blank' + document.body.appendChild(iframe) + const doc = iframe.contentDocument! + doc.open() + doc.write(innerHTML) + doc.close() + const ready = new Promise((resolve) => { + window.addEventListener('message', (e) => { + if (e.source === iframe.contentWindow && e.data === 'iframe_ready') { + iframe.contentWindow!.postMessage({ + type: 'init', + }) + resolve(undefined) + } + }) + }) + const blob = new Blob([jsScriptContent], { type: 'application/javascript' }) + const runtimeScript = doc.createElement('script') + runtimeScript.src = URL.createObjectURL(blob) + doc.head.appendChild(runtimeScript) + this.iframeReady = ready + this.iframe = iframe + } + + private buildIframeInnerHTMLWithScriptTag(includedEdgeFn = true): string { + return [ + ``, + ``, + ``, + includedEdgeFn + ? `` + : '', + ``, + ` + `, + ].join(',') + } + + private getIframeScriptJSScriptContent(processSignalFn?: string): string { + // External signal processor script + // Inject runtime via Blob (CSP-safe) + return ` + ${processSignalFn ? `window.processSignal = ${processSignalFn}` : ''} + + const signalsScript = document.getElementById('edge-fn') + if (typeof processSignal === 'undefined') { + signalsScript.onload = () => { + window.parent.postMessage('iframe_ready') + } + } else { + window.parent.postMessage('iframe_ready') + } + + class AnalyticsRuntimeProxy { + constructor() { + this.calls = new Map(); + } + getCalls() { + return Object.fromEntries(this.calls); // call in {track: [args]} format + } + createProxy() { + return new Proxy({}, { + get: (_, methodName) => { + return (...args) => { + if (!this.calls.has(methodName)) { + this.calls.set(methodName, []); + } + this.calls.get(methodName).push(args); + }; + }, + }); + } + } + + + // expose the signals global + ${getRuntimeCode()} + + window.addEventListener('message', async (event) => { + const { type, payload } = event.data; + + + if (type === 'execute') { + try { + const analyticsProxy = new AnalyticsRuntimeProxy(); + window.analytics = analyticsProxy.createProxy(); + if (!payload.signal) { + throw new Error('invariant: no signal found') + } + if (!payload.signalBuffer) { + throw new Error('invariant: no signalBuffer found') + } + if (!payload.constants) { + throw new Error('invariant: no constants found') + } + if (typeof processSignal === 'undefined') { + throw new Error('processSignal is undefined') + } + + const signalBuffer = payload.signalBuffer + const signal = payload.signal + const constants = payload.constants + Object.entries(constants).forEach(([key, value]) => { // expose constants as globals + window[key] = value; + }); + window.signals.signalBuffer = signalBuffer; // signals is exposed as part of get runtimeCode + window.processSignal(signal, { signals, constants }) + event.source.postMessage({ type: 'execution_result', payload: analyticsProxy.getCalls() }); + } catch(err) { + event.source.postMessage({ type: 'execution_error', error: err }); + } + } + }); + ` + } + private normalizeAnalyticsMethodCallsWithArgResolver = ( + methodCalls: AnalyticsMethodCalls + ) => { + const analytics = new AnalyticsRuntime() + Object.entries(methodCalls).forEach(([methodName, calls]) => { + calls.forEach((args) => { + // @ts-ignore + analytics[methodName](...args) + }) + }) + return analytics.getCalls() + } + + async execute( + signal: Signal, + signals: Signal[] + ): Promise { + await this.iframeReady + + return new Promise((resolve, reject) => { + const handler = (e: MessageEvent) => { + if (e.source !== this.iframe.contentWindow) return + if (e.data?.type === 'execution_result') { + window.removeEventListener('message', handler) + const methodCalls = this.normalizeAnalyticsMethodCallsWithArgResolver( + e.data.payload + ) + resolve(methodCalls) + } + if (e.data?.type === 'execution_error') { + window.removeEventListener('message', handler) + reject(e.data.error) + } + } + + window.addEventListener('message', handler) + + this.iframe.contentWindow!.postMessage({ + type: 'execute', + payload: { + signal, + signalBuffer: signals, + constants: WebRuntimeConstants, + }, + }) + }) + } + + destroy() { + this.iframe.remove() + } +} diff --git a/packages/signals/signals/src/lib/workerbox/__mocks__/workerbox.ts b/packages/signals/signals/src/lib/workerbox/__mocks__/workerbox.ts deleted file mode 100644 index 04a9585fc..000000000 --- a/packages/signals/signals/src/lib/workerbox/__mocks__/workerbox.ts +++ /dev/null @@ -1,13 +0,0 @@ -// __mocks__/workerbox.ts - -const mockRun = jest.fn(() => Promise.resolve('mocked response')) -const mockDestroy = jest.fn() - -const createWorkerBox = jest.fn(() => { - return Promise.resolve({ - run: mockRun, - destroy: mockDestroy, - }) -}) - -export { createWorkerBox } diff --git a/packages/signals/signals/src/lib/workerbox/index.ts b/packages/signals/signals/src/lib/workerbox/index.ts deleted file mode 100644 index ec1d9742e..000000000 --- a/packages/signals/signals/src/lib/workerbox/index.ts +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Copyright 2022 Mark Wylde - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ - -import { default as worker } from './worker.generated' - -/** - * Whenever this script is modified, we need to run `yarn workerbox` and commit the result. - * This happens via lint-staged (on commit that modifiies the enclosing folder), so it should be automatic. - */ -export function stringToScope( - object: any, - addCallback: Function, - runCallback: Function -) { - return decodeArg(JSON.parse(object), addCallback, runCallback) -} - -// This was modified nd adopted from ^ workerbox library - https://github.com/markwylde/workerbox/blob/master/lib/index.js -// note: we could write our own, but this has always been tested and works -// the types are bad because the workerbox library src is not typed, and I did not want to spend a lot of time writing types. -const generateUniqueId = () => { - // @ts-ignore - globalThis.workerboxIncrementor = (globalThis.workerboxIncrementor || 0) + 1 - return ( - // @ts-ignore - globalThis.workerboxIncrementor + - '_' + - Array(20) - .fill( - '!@#$%^&*()_+-=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' - ) - .map((x) => { - return x[Math.floor(Math.random() * x.length)] - }) - .join('') - ) -} - -export function createCallbackStore() { - const store: Record = {} - const add = (fn: Function) => { - const id = generateUniqueId() - store[id] = fn - return id - } - const get = (id: string) => { - return store[id] - } - return { - store, - add, - get, - } -} - -function encodeArg( - arg: any, - addCallback: Function, - runCallback: Function -): ['callback' | 'object' | 'literal' | 'array', any] { - if (typeof arg === 'function') { - return ['callback', addCallback(arg)] - } else if (arg instanceof Array) { - return [ - 'array', - arg.map((arg2) => encodeArg(arg2, addCallback, runCallback)), - ] - } else if (typeof arg === 'object' && arg !== null) { - const newArg: Record = {} - for (const key in arg) { - newArg[key] = encodeArg(arg[key], addCallback, runCallback) - } - return ['object', newArg] - } else { - return ['literal', arg] - } -} - -export function scopeToString( - scope: any, - addCallback: Function, - runCallback: Function -) { - return JSON.stringify(encodeArg(scope || {}, addCallback, runCallback)) -} - -export function argsToString( - args: any, - addCallback: Function, - runCallback: Function -): string { - return JSON.stringify(encodeArg(args, addCallback, runCallback)) -} - -function decodeArg(arg: any, addCallback: Function, runCallback: Function) { - if (arg[0] === 'callback') { - return (...args: any[]) => - runCallback(arg[1], argsToString(args, addCallback, runCallback)) - } else if (arg[0] === 'array') { - return arg[1].map((arg: any) => decodeArg(arg, addCallback, runCallback)) - } else if (arg[0] === 'object') { - const decodedArg: Record = {} - for (const key in arg[1]) { - decodedArg[key] = decodeArg(arg[1][key], addCallback, runCallback) - } - return decodedArg - } else if (arg[0] === 'literal') { - return arg[1] - } else { - throw Error(`Unexpected arg type: ${arg[0]}`) - } -} - -export function stringToArgs( - args: any, - addCallback: Function, - runCallback: Function -) { - return decodeArg(JSON.parse(args), addCallback, runCallback) -} - -const instances = { - count: 0, -} -export function createWorkerboxInstance( - url: string | undefined, - onMessage: MessagePort['onmessage'] -) { - instances.count = instances.count + 1 - const channel = new MessageChannel() - const iframe = document.createElement('iframe') - iframe.setAttribute('sandbox', 'allow-scripts') - iframe.id = `seg-workerbox-${instances.count}` - - // display none is not enough - iframe.setAttribute( - 'style', - 'position: fixed; height: 0; width: 0; opacity: 0; top: -100px;' - ) - if (url) { - iframe.src = url - } else { - iframe.srcdoc = worker.toString() - } - document.body.appendChild(iframe) - channel.port1.onmessage = onMessage - return new Promise<{ - postMessage: (message: any) => void - destroy: () => void - }>((resolve) => { - iframe.addEventListener('load', () => { - if (!iframe.contentWindow) { - throw new Error('iframe.contentWindow is null') - } - iframe.contentWindow.postMessage('OK', '*', [channel.port2]) - resolve({ - postMessage: (message) => channel.port1.postMessage(message), - destroy: () => iframe.remove(), - }) - }) - }) -} - -export interface WorkerBoxOptions { - url?: string -} - -export interface WorkerBoxAPI { - run: (code: string, scope: Record) => Promise - destroy: () => void - opts?: WorkerBoxOptions -} - -async function createWorkerBox( - opts: WorkerBoxOptions = {} -): Promise { - if (opts.url && opts.url.slice(-1) === '/') { - opts.url = opts.url.slice(0, -1) - } - - opts.url = opts.url && new URL(opts.url).href - const callbacks = createCallbackStore() - const run = (id: string, args: any) => - new Promise((resolve, reject) => { - instance.postMessage([ - 'callback', - { - id, - args, - resolve: callbacks.add(resolve), - reject: callbacks.add(reject), - }, - ]) - }) - const instance = await createWorkerboxInstance(opts.url, async (message) => { - const [action, { id, args, resolve, reject }] = message.data - const parsedArgs = stringToArgs(args, callbacks.add, run) - if (action === 'error') { - callbacks.get(id)?.(new Error(parsedArgs[0])) - return - } - if (action === 'return') { - callbacks.get(id)?.(parsedArgs[0]) - return - } - const fn = callbacks.get(id) - if (!fn) { - return - } - try { - const result = await fn(...parsedArgs) - instance.postMessage([ - 'callback', - { - id: resolve, - args: argsToString([result], callbacks.add, run), - }, - ]) - } catch (error) { - const message = error instanceof Error ? error.message : error - instance.postMessage([ - 'callback', - { - id: reject, - args: argsToString([message], callbacks.add, run), - }, - ]) - } - }) - return { - run: async (code, originalScope) => { - return new Promise((resolve, reject) => { - const id = callbacks.add(resolve) - const errorId = callbacks.add(reject) - const scope = scopeToString(originalScope, callbacks.add, run) - instance.postMessage(['execute', { id, errorId, code, scope }]) - }) - }, - destroy: () => instance.destroy(), - opts, - } -} - -export { createWorkerBox } diff --git a/packages/signals/signals/src/lib/workerbox/worker.generated.ts b/packages/signals/signals/src/lib/workerbox/worker.generated.ts deleted file mode 100644 index 232281d65..000000000 --- a/packages/signals/signals/src/lib/workerbox/worker.generated.ts +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable */ -// built from /dist/worker.html -export default atob('PHNjcmlwdD4KICBmdW5jdGlvbiB3b3JrZXJTY3JpcHQoKSB7CiAgICAidXNlIHN0cmljdCI7KCgpPT57ZnVuY3Rpb24gYihlLHIsdCl7cmV0dXJuIHAoSlNPTi5wYXJzZShlKSxyLHQpfXZhciBTPSgpPT4oZ2xvYmFsVGhpcy53b3JrZXJib3hJbmNyZW1lbnRvcj0oZ2xvYmFsVGhpcy53b3JrZXJib3hJbmNyZW1lbnRvcnx8MCkrMSxnbG9iYWxUaGlzLndvcmtlcmJveEluY3JlbWVudG9yKyJfIitBcnJheSgyMCkuZmlsbCgiIUAjJCVeJiooKV8rLT0wMTIzNDU2Nzg5QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eiIpLm1hcChlPT5lW01hdGguZmxvb3IoTWF0aC5yYW5kb20oKSplLmxlbmd0aCldKS5qb2luKCIiKSk7ZnVuY3Rpb24geCgpe2xldCBlPXt9O3JldHVybntzdG9yZTplLGFkZDpuPT57bGV0IG89UygpO3JldHVybiBlW29dPW4sb30sZ2V0Om49PmVbbl19fWZ1bmN0aW9uIG0oZSxyLHQpe2lmKHR5cGVvZiBlPT0iZnVuY3Rpb24iKXJldHVyblsiY2FsbGJhY2siLHIoZSldO2lmKGUgaW5zdGFuY2VvZiBBcnJheSlyZXR1cm5bImFycmF5IixlLm1hcChuPT5tKG4scix0KSldO2lmKHR5cGVvZiBlPT0ib2JqZWN0IiYmZSE9PW51bGwpe2xldCBuPXt9O2ZvcihsZXQgbyBpbiBlKW5bb109bShlW29dLHIsdCk7cmV0dXJuWyJvYmplY3QiLG5dfWVsc2UgcmV0dXJuWyJsaXRlcmFsIixlXX1mdW5jdGlvbiBhKGUscix0KXtyZXR1cm4gSlNPTi5zdHJpbmdpZnkobShlLHIsdCkpfWZ1bmN0aW9uIHAoZSxyLHQpe2lmKGVbMF09PT0iY2FsbGJhY2siKXJldHVybiguLi5uKT0+dChlWzFdLGEobixyLHQpKTtpZihlWzBdPT09ImFycmF5IilyZXR1cm4gZVsxXS5tYXAobj0+cChuLHIsdCkpO2lmKGVbMF09PT0ib2JqZWN0Iil7bGV0IG49e307Zm9yKGxldCBvIGluIGVbMV0pbltvXT1wKGVbMV1bb10scix0KTtyZXR1cm4gbn1lbHNle2lmKGVbMF09PT0ibGl0ZXJhbCIpcmV0dXJuIGVbMV07dGhyb3cgRXJyb3IoYFVuZXhwZWN0ZWQgYXJnIHR5cGU6ICR7ZVswXX1gKX19ZnVuY3Rpb24gayhlLHIsdCl7cmV0dXJuIHAoSlNPTi5wYXJzZShlKSxyLHQpfWFzeW5jIGZ1bmN0aW9uIEUoZSxyKXtyZXR1cm4gYXdhaXQgRnVuY3Rpb24uYXBwbHkobnVsbCxbLi4uT2JqZWN0LmtleXMoZSksYHJldHVybiAoYXN5bmMgZnVuY3Rpb24gc2FuZGJveCAoKSB7JHtyfSB9KSgpYF0pLmFwcGx5KG51bGwsWy4uLk9iamVjdC52YWx1ZXMoZSldKX12YXIgdz0oZSxyKT0+e2lmKCFlLnN0YWNrKXJldHVybiBlLm1lc3NhZ2U7bGV0IHQ9ZS5zdGFjay5zcGxpdChgCmApLG49cz0+dHlwZW9mIHM9PSJzdHJpbmciP3BhcnNlSW50KHMsMTApOnM7cmV0dXJuW3RbMF0sLi4udC5maWx0ZXIocz0+cy5pbmNsdWRlcygiKGV2YWwgYXQgc2NvcGVkRXZhbCIpKS5tYXAocz0+e2xldCBkPXMuc3BsaXQoIihldmFsIGF0IHNjb3BlZEV2YWwgKCIpLFssbF09cy5zcGxpdCgiPGFub255bW91cz4iKSxbLGcsZl09bC5zbGljZSgwLC0xKS5zcGxpdCgiOiIpO3JldHVybmAke2RbMF19KDxzYW5kYm94Pjoke24oZyktM306JHtmfSlgfSldLnNsaWNlKDAscikuam9pbihgCmApfTtzZWxmLmFkZEV2ZW50TGlzdGVuZXIoIm1lc3NhZ2UiLGU9PntsZXQgcj1lLnBvcnRzWzBdLHQ9eCgpLG49KG8scyk9Pm5ldyBQcm9taXNlKGQ9PntyLnBvc3RNZXNzYWdlKFsiY2FsbGJhY2siLHtpZDpvLGFyZ3M6cyxyZXNvbHZlOnQuYWRkKGQpfV0pfSk7ci5vbm1lc3NhZ2U9YXN5bmMgbz0+e2xldFtzLGRdPW8uZGF0YSx7aWQ6bCxlcnJvcklkOmcsY29kZTpmLHNjb3BlOmgsYXJnczp2LHJlc29sdmU6TSxyZWplY3Q6Rn09ZDtpZihzPT09ImV4ZWN1dGUiKXtsZXQgeT1iKGgsdC5hZGQsbik7dHJ5e2xldCBpPWF3YWl0IEUoeSxmKTtyLnBvc3RNZXNzYWdlKFsicmV0dXJuIix7aWQ6bCxhcmdzOmEoW2ldLHQuYWRkLG4pfV0pfWNhdGNoKGkpe2xldCBjPWk7dHJ5e2xldCB1PXcoYywtMSk7ci5wb3N0TWVzc2FnZShbImVycm9yIix7aWQ6ZyxhcmdzOmEoW3V8fGMubWVzc2FnZV0sdC5hZGQsbil9XSl9Y2F0Y2h7ci5wb3N0TWVzc2FnZShbImVycm9yIix7aWQ6ZyxhcmdzOmEoW2MubWVzc2FnZV0sdC5hZGQsbil9XSl9fX1pZihzPT09ImNhbGxiYWNrIil7bGV0IHk9ayh2LHQuYWRkLG4pLGk9dC5nZXQobCk7aWYoIWkpcmV0dXJuO3RyeXtsZXQgYz1hd2FpdCBpKC4uLnkpO3IucG9zdE1lc3NhZ2UoWyJyZXR1cm4iLHtpZDpNLGFyZ3M6YShbY10sdC5hZGQsbil9XSl9Y2F0Y2goYyl7bGV0IHU9YyxBPXcodSk7ci5wb3N0TWVzc2FnZShbImVycm9yIix7aWQ6RixhcmdzOmEoW0F8fHUubWVzc2FnZV0sdC5hZGQsbil9XSl9fX19KTt9KSgpOwoKICB9CgogIHNlbGYuYWRkRXZlbnRMaXN0ZW5lcignbWVzc2FnZScsIGFzeW5jIChldmVudCkgPT4gewogICAgY29uc3QgY29kZSA9IHdvcmtlclNjcmlwdC50b1N0cmluZygpLnNwbGl0KCdcbicpLnNsaWNlKDEsIC0xKS5qb2luKCdcbicpOwogICAgY29uc3QgYmxvYiA9IG5ldyBCbG9iKFtjb2RlXSwgeyB0eXBlOiAnYXBwbGljYXRpb24vamF2YXNjcmlwdCcgfSkKICAgIGNvbnN0IHdvcmtlciA9IG5ldyBXb3JrZXIoVVJMLmNyZWF0ZU9iamVjdFVSTChibG9iKSkKCiAgICB3b3JrZXIucG9zdE1lc3NhZ2UoJ09LJywgZXZlbnQucG9ydHMpOwogIH0pOwo8L3NjcmlwdD4='); \ No newline at end of file diff --git a/packages/signals/signals/src/lib/workerbox/worker.html b/packages/signals/signals/src/lib/workerbox/worker.html deleted file mode 100644 index a5808c204..000000000 --- a/packages/signals/signals/src/lib/workerbox/worker.html +++ /dev/null @@ -1,13 +0,0 @@ - \ No newline at end of file diff --git a/packages/signals/signals/src/lib/workerbox/worker.ts b/packages/signals/signals/src/lib/workerbox/worker.ts deleted file mode 100644 index 74cdba8ef..000000000 --- a/packages/signals/signals/src/lib/workerbox/worker.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { - createCallbackStore, - argsToString, - stringToScope, - stringToArgs, -} from './index' -async function scopedEval(context: any, expr: string) { - const evaluator = Function.apply(null, [ - ...Object.keys(context), - `return (async function sandbox () {${expr} })()`, - ]) - return await evaluator.apply(null, [...Object.values(context)]) -} - -const getStack = (error: Error, slice?: number) => { - if (!error.stack) { - return error.message - } - const lines: string[] = error.stack.split('\n') - const parseNumber = (num: string | number) => - typeof num === 'string' ? parseInt(num, 10) : num - const stack = [ - lines[0], - ...lines - .filter((line) => line.includes('(eval at scopedEval')) - .map((line) => { - const splitted = line.split('(eval at scopedEval (') - const [, mixedPosition] = line.split('') - const [, lineNumber, charNumber] = mixedPosition.slice(0, -1).split(':') - return `${splitted[0]}(:${ - parseNumber(lineNumber) - 3 - }:${charNumber})` - }), - ] - .slice(0, slice) - .join('\n') - return stack -} - -self.addEventListener('message', (event) => { - const port = event.ports[0] - - const callbacks = createCallbackStore() - const run = (id: string, args: any) => - new Promise((resolve) => { - port.postMessage([ - 'callback', - { id, args, resolve: callbacks.add(resolve) }, - ]) - }) - - port.onmessage = async (event) => { - const [action, message] = event.data - const { id, errorId, code, scope, args, resolve, reject } = message - - if (action === 'execute') { - const parsedScope = stringToScope(scope, callbacks.add, run) - - try { - const result = await scopedEval(parsedScope, code) - - port.postMessage([ - 'return', - { id, args: argsToString([result], callbacks.add, run) }, - ]) - } catch (e) { - const error = e as Error - try { - const stack = getStack(error, -1) - port.postMessage([ - 'error', - { - id: errorId, - args: argsToString([stack || error.message], callbacks.add, run), - }, - ]) - } catch (error2) { - port.postMessage([ - 'error', - { - id: errorId, - args: argsToString([error.message], callbacks.add, run), - }, - ]) - } - } - } - - if (action === 'callback') { - const parsedArgs = stringToArgs(args, callbacks.add, run) - - const fn = callbacks.get(id) - if (!fn) { - return - } - try { - const result = await fn(...parsedArgs) - port.postMessage([ - 'return', - { id: resolve, args: argsToString([result], callbacks.add, run) }, - ]) - } catch (e) { - const error = e as Error - const stack = getStack(error) - port.postMessage([ - 'error', - { - id: reject, - args: argsToString([stack || error.message], callbacks.add, run), - }, - ]) - } - } - } -}) diff --git a/packages/signals/signals/src/types/process-signal.ts b/packages/signals/signals/src/types/process-signal.ts index 0f8b2bfd8..866bc22d8 100644 --- a/packages/signals/signals/src/types/process-signal.ts +++ b/packages/signals/signals/src/types/process-signal.ts @@ -20,7 +20,8 @@ export interface AnalyticsRuntimePublicApi { export type ProcessSignalScope = { analytics: AnalyticsRuntimePublicApi signals: SignalsRuntime -} & typeof WebRuntimeConstants + constants: typeof WebRuntimeConstants +} export interface ProcessSignal { (signal: Signal, ctx: ProcessSignalScope): void