From ba2f2b165bf1b997a9ce79d410690d27d50378fd Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:14:30 -0500 Subject: [PATCH] Signals analytics runtime (#1168) --- .changeset/proud-meals-cheat.md | 4 + .changeset/proud-meals-ox.md | 4 + meta-tests/check-dts.ts | 8 +- package.json | 1 + .../src/helpers/base-page-object.ts | 9 +- .../src/helpers/network-utils.ts | 26 ++ .../src/tests/signals-vanilla/basic.test.ts | 6 +- .../button-click-complex.test.ts | 6 +- .../signals-vanilla/change-input.test.ts | 4 +- .../network-signals-allow-list.test.ts | 4 +- .../network-signals-fetch.test.ts | 14 +- .../network-signals-xhr.test.ts | 31 +-- .../signals-vanilla/runtime-constants.test.ts | 44 ++++ .../signals-vanilla/signals-redaction.test.ts | 7 +- packages/signals/signals-runtime/.eslintrc.js | 7 + .../signals/signals-runtime/.lintstagedrc.js | 1 + packages/signals/signals-runtime/LICENSE | 21 ++ packages/signals/signals-runtime/README.md | 10 + .../signals-runtime/api-extractor.base.json | 33 +++ .../signals-runtime/api-extractor.mobile.json | 8 + .../signals-runtime/api-extractor.web.json | 8 + .../signals-runtime/build-editor-types.js | 48 ++++ .../build-signals-runtime-global.js | 106 ++++++++ .../bundle-build-tests/runtime-env.test.ts | 69 ++++++ .../signals/signals-runtime/jest.config.js | 6 + .../signals/signals-runtime/jest.setup.js | 9 + packages/signals/signals-runtime/package.json | 49 ++++ .../src/__tests__/signals-runtime.test.ts | 111 +++++++++ packages/signals/signals-runtime/src/index.ts | 17 ++ .../src/mobile/get-runtime-code.generated.ts | 8 + .../src/mobile/index.mobile-editor.ts | 11 + .../src/mobile/index.signals-runtime.ts | 13 + .../src/mobile/mobile-constants.ts | 40 +++ .../src/mobile/mobile-signals-runtime.ts | 33 +++ .../src/mobile/mobile-signals-types.ts | 90 +++++++ .../src/shared/shared-types.ts | 21 ++ .../src/shared/signals-runtime.ts | 52 ++++ .../mocks/mock-signal-types-web.ts | 54 ++++ .../src/web/get-runtime-code.generated.ts | 8 + .../src/web/index.signals-runtime.ts | 11 + .../src/web/index.web-editor.ts | 11 + .../signals-runtime/src/web/web-constants.ts | 22 ++ .../src/web/web-signals-runtime.ts | 4 + .../src/web/web-signals-types.ts | 101 ++++++++ .../signals-runtime/tsconfig.build.json | 9 + .../signals/signals-runtime/tsconfig.json | 12 + packages/signals/signals/package.json | 3 +- .../src/core/analytics-service/index.ts | 3 +- .../src/core/buffer/__tests__/buffer.test.ts | 2 +- .../signals/signals/src/core/buffer/index.ts | 2 +- .../src/core/client/__tests__/redact.test.ts | 20 +- .../signals/signals/src/core/client/index.ts | 2 +- .../signals/signals/src/core/client/redact.ts | 2 +- .../signals/signals/src/core/emitter/index.ts | 2 +- .../__tests__/signals-runtime.test.ts | 56 ----- .../signals/src/core/processor/polyfills.ts | 16 ++ .../signals/src/core/processor/processor.ts | 3 +- .../signals/src/core/processor/sandbox.ts | 12 +- .../src/core/processor/signals-runtime.ts | 42 ---- .../src/core/signal-generators/dom-gen.ts | 5 +- .../signal-generators/network-gen/index.ts | 2 +- .../signals/src/core/signals/signals.ts | 3 +- packages/signals/signals/src/index.ts | 3 +- packages/signals/signals/src/index.umd.ts | 4 - .../signals/src/plugin/signals-plugin.ts | 3 +- .../__tests__/create-network-signal.test.ts | 4 +- .../signals/signals/src/types/factories.ts | 74 ++++++ packages/signals/signals/src/types/index.ts | 1 - packages/signals/signals/src/types/json.ts | 4 - .../signals/src/types/process-signal.ts | 32 +-- packages/signals/signals/src/types/signals.ts | 186 -------------- turbo.json | 15 ++ yarn.lock | 231 +++++++++++++++++- 73 files changed, 1495 insertions(+), 408 deletions(-) create mode 100644 .changeset/proud-meals-cheat.md create mode 100644 .changeset/proud-meals-ox.md create mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/runtime-constants.test.ts create mode 100644 packages/signals/signals-runtime/.eslintrc.js create mode 100644 packages/signals/signals-runtime/.lintstagedrc.js create mode 100644 packages/signals/signals-runtime/LICENSE create mode 100644 packages/signals/signals-runtime/README.md create mode 100644 packages/signals/signals-runtime/api-extractor.base.json create mode 100644 packages/signals/signals-runtime/api-extractor.mobile.json create mode 100644 packages/signals/signals-runtime/api-extractor.web.json create mode 100644 packages/signals/signals-runtime/build-editor-types.js create mode 100644 packages/signals/signals-runtime/build-signals-runtime-global.js create mode 100644 packages/signals/signals-runtime/bundle-build-tests/runtime-env.test.ts create mode 100644 packages/signals/signals-runtime/jest.config.js create mode 100644 packages/signals/signals-runtime/jest.setup.js create mode 100644 packages/signals/signals-runtime/package.json create mode 100644 packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts create mode 100644 packages/signals/signals-runtime/src/index.ts create mode 100644 packages/signals/signals-runtime/src/mobile/get-runtime-code.generated.ts create mode 100644 packages/signals/signals-runtime/src/mobile/index.mobile-editor.ts create mode 100644 packages/signals/signals-runtime/src/mobile/index.signals-runtime.ts create mode 100644 packages/signals/signals-runtime/src/mobile/mobile-constants.ts create mode 100644 packages/signals/signals-runtime/src/mobile/mobile-signals-runtime.ts create mode 100644 packages/signals/signals-runtime/src/mobile/mobile-signals-types.ts create mode 100644 packages/signals/signals-runtime/src/shared/shared-types.ts create mode 100644 packages/signals/signals-runtime/src/shared/signals-runtime.ts create mode 100644 packages/signals/signals-runtime/src/test-helpers/mocks/mock-signal-types-web.ts create mode 100644 packages/signals/signals-runtime/src/web/get-runtime-code.generated.ts create mode 100644 packages/signals/signals-runtime/src/web/index.signals-runtime.ts create mode 100644 packages/signals/signals-runtime/src/web/index.web-editor.ts create mode 100644 packages/signals/signals-runtime/src/web/web-constants.ts create mode 100644 packages/signals/signals-runtime/src/web/web-signals-runtime.ts create mode 100644 packages/signals/signals-runtime/src/web/web-signals-types.ts create mode 100644 packages/signals/signals-runtime/tsconfig.build.json create mode 100644 packages/signals/signals-runtime/tsconfig.json delete mode 100644 packages/signals/signals/src/core/processor/__tests__/signals-runtime.test.ts create mode 100644 packages/signals/signals/src/core/processor/polyfills.ts delete mode 100644 packages/signals/signals/src/core/processor/signals-runtime.ts create mode 100644 packages/signals/signals/src/types/factories.ts delete mode 100644 packages/signals/signals/src/types/json.ts delete mode 100644 packages/signals/signals/src/types/signals.ts diff --git a/.changeset/proud-meals-cheat.md b/.changeset/proud-meals-cheat.md new file mode 100644 index 000000000..16a56dfde --- /dev/null +++ b/.changeset/proud-meals-cheat.md @@ -0,0 +1,4 @@ +--- +'@segment/analytics-signals-runtime': major +--- +Release analytics-signals-runtime diff --git a/.changeset/proud-meals-ox.md b/.changeset/proud-meals-ox.md new file mode 100644 index 000000000..1cd1f9c62 --- /dev/null +++ b/.changeset/proud-meals-ox.md @@ -0,0 +1,4 @@ +--- +'@segment/analytics-signals': minor +--- +Refactor runtime to use `@segment/analytics-signals-runtime` diff --git a/meta-tests/check-dts.ts b/meta-tests/check-dts.ts index d48352699..90adac3ac 100644 --- a/meta-tests/check-dts.ts +++ b/meta-tests/check-dts.ts @@ -11,7 +11,13 @@ import fs from 'fs' */ const execa = promisify(exec) -const allPublicPackageDirNames = ['browser', 'core', 'node'] as const +const allPublicPackageDirNames = [ + 'browser', + 'core', + 'node', + 'signals/signals', + 'signals/signals-runtime', +] as const type PackageDirName = typeof allPublicPackageDirNames[number] diff --git a/package.json b/package.json index 417cb53d2..43c93ae6d 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "devDependencies": { "@changesets/changelog-github": "^0.4.5", "@changesets/cli": "^2.23.2", + "@microsoft/api-extractor": "^7.47.9", "@npmcli/promise-spawn": "^7.0.0", "@types/express": "4", "@types/jest": "^29.5.11", diff --git a/packages/signals/signals-integration-tests/src/helpers/base-page-object.ts b/packages/signals/signals-integration-tests/src/helpers/base-page-object.ts index f2b3004e6..6f3566462 100644 --- a/packages/signals/signals-integration-tests/src/helpers/base-page-object.ts +++ b/packages/signals/signals-integration-tests/src/helpers/base-page-object.ts @@ -31,6 +31,7 @@ export class BasePage { */ async loadAndWait(...args: Parameters) { await Promise.all([this.load(...args), this.waitForSettings()]) + return this } /** @@ -48,8 +49,12 @@ export class BasePage { this.edgeFn = edgeFn await this.setupMockedRoutes() const url = options.updateURL ? options.updateURL(this.url) : this.url - await this.page.goto(url) - void this.invokeAnalyticsLoad(signalSettings) + await this.page.goto(url, { waitUntil: 'domcontentloaded' }) + void this.invokeAnalyticsLoad({ + flushInterval: 500, + ...signalSettings, + }) + return this } /** diff --git a/packages/signals/signals-integration-tests/src/helpers/network-utils.ts b/packages/signals/signals-integration-tests/src/helpers/network-utils.ts index 3b5db7e22..d9f0651bf 100644 --- a/packages/signals/signals-integration-tests/src/helpers/network-utils.ts +++ b/packages/signals/signals-integration-tests/src/helpers/network-utils.ts @@ -1,6 +1,7 @@ import { Page, Route, Request } from '@playwright/test' import { SegmentEvent } from '@segment/analytics-next' import { Signal } from '@segment/analytics-signals' +import { waitForCondition } from './playwright-utils' type FulfillOptions = Parameters['0'] export interface XHRRequestOptions { @@ -160,6 +161,31 @@ export class TrackingAPIRequestBuffer { } export class SignalAPIRequestBuffer extends TrackingAPIRequestBuffer { + async waitForEvents( + numberOfSignals: number, + signalType?: Signal['type'] + ): Promise { + await waitForCondition( + () => this.getEvents(signalType).length >= numberOfSignals, + { + timeout: 5000, + errorMessage: `Found ${ + this.getEvents(signalType).length + } signals, expected ${numberOfSignals}`, + } + ) + const events = this.getEvents(signalType) + if (events.length < numberOfSignals) { + throw new Error( + `Expected ${numberOfSignals} signals of type ${signalType}, but got ${ + this.getEvents(signalType).length + }` + ) + } else { + return events + } + } + override getEvents(signalType?: Signal['type']): SegmentEvent[] { if (signalType) { return this.getEvents().filter((e) => e.properties!.type === signalType) 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 ccc5bb578..2e1aea0e8 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 @@ -1,8 +1,6 @@ import { test, expect } from '@playwright/test' import { IndexPage } from './index-page' -const indexPage = new IndexPage() - const basicEdgeFn = ` // this is a process signal function const processSignal = (signal) => { @@ -12,8 +10,10 @@ const basicEdgeFn = ` } }` +let indexPage: IndexPage + test.beforeEach(async ({ page }) => { - await indexPage.loadAndWait(page, basicEdgeFn) + indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn) }) test('network signals fetch', async () => { 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 2be92fede..28eb61c32 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,12 +1,10 @@ import { test, expect } from '@playwright/test' import { IndexPage } from './index-page' -const indexPage = new IndexPage() - const basicEdgeFn = `const processSignal = (signal) => {}` - +let indexPage: IndexPage test.beforeEach(async ({ page }) => { - await indexPage.loadAndWait(page, basicEdgeFn) + indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn) }) const data = { 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 a44682508..fb8e9fccc 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,8 +2,6 @@ import { test, expect } from '@playwright/test' import { waitForCondition } from '../../helpers/playwright-utils' import { IndexPage } from './index-page' -const indexPage = new IndexPage() - const basicEdgeFn = `const processSignal = (signal) => {}` test('Collecting signals whenever a user enters text input', async ({ @@ -12,7 +10,7 @@ test('Collecting signals whenever a user enters text input', async ({ /** * Input some text into the input field, see if the signal is emitted correctly */ - await indexPage.loadAndWait(page, basicEdgeFn, { + const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { disableSignalsRedaction: true, enableSignalsIngestion: true, }) 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 38dbf24d8..84323ac04 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,12 +1,10 @@ import { test, expect } from '@playwright/test' import { IndexPage } from './index-page' -const indexPage = new IndexPage() - const basicEdgeFn = `const processSignal = (signal) => {}` test('network signals allow and disallow list', async ({ page }) => { - await indexPage.loadAndWait(page, basicEdgeFn, { + const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { networkSignalsAllowList: ['allowed-api.com'], networkSignalsDisallowList: ['https://disallowed-api.com/api/foo'], }) 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 6b27d56ac..8b2739064 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 @@ -7,8 +7,7 @@ test.describe('network signals - fetch', () => { let indexPage: IndexPage test.beforeEach(async ({ page }) => { - indexPage = new IndexPage() - await indexPage.loadAndWait(page, basicEdgeFn) + indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn) }) test('should not emit anything if neither request nor response are json', async () => { @@ -55,9 +54,10 @@ test.describe('network signals - fetch', () => { (el) => el.properties!.data.action === 'request' ) expect(requests).toHaveLength(1) - expect(requests[0].properties!.data).toMatchObject({ + expect(requests[0].properties!.data).toEqual({ action: 'request', url: 'http://localhost/test', + method: 'POST', data: { key: 'value' }, }) @@ -65,10 +65,12 @@ test.describe('network signals - fetch', () => { (el) => el.properties!.data.action === 'response' ) expect(responses).toHaveLength(1) - expect(responses[0].properties!.data).toMatchObject({ + expect(responses[0].properties!.data).toEqual({ action: 'response', url: 'http://localhost/test', data: { foo: 'test' }, + status: 200, + ok: true, }) }) @@ -177,6 +179,8 @@ test.describe('network signals - fetch', () => { action: 'response', url: 'http://localhost/test', data: { errorMsg: 'foo' }, + status: 400, + ok: false, }) expect(responses).toHaveLength(1) }) @@ -214,6 +218,8 @@ test.describe('network signals - fetch', () => { action: 'response', url: 'http://localhost/test', data: 'foo', + status: 400, + ok: false, }) expect(responses).toHaveLength(1) }) 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 9936a8ac5..17f20c3e8 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,5 +1,6 @@ import { test, expect } from '@playwright/test' import { IndexPage } from './index-page' +import { sleep } from '@segment/analytics-core' const basicEdgeFn = `const processSignal = (signal) => {}` @@ -7,8 +8,7 @@ test.describe('network signals - XHR', () => { let indexPage: IndexPage test.beforeEach(async ({ page }) => { - indexPage = new IndexPage() - await indexPage.loadAndWait(page, basicEdgeFn) + indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn) }) test('should not emit anything if neither request nor response are json', async () => { await indexPage.network.mockTestRoute('http://localhost/test', { @@ -24,7 +24,7 @@ test.describe('network signals - XHR', () => { }) // Wait for the signals to be flushed - await indexPage.waitForSignalsApiFlush() + await sleep(300) const networkEvents = indexPage.signalsAPI.getEvents('network') @@ -46,10 +46,7 @@ test.describe('network signals - XHR', () => { expect(data).toEqual({ foo: 'test' }) - // Wait for the signals to be flushed - await indexPage.waitForSignalsApiFlush() - - const networkEvents = indexPage.signalsAPI.getEvents('network') + const networkEvents = await indexPage.signalsAPI.waitForEvents(2, 'network') // Check the request const requests = networkEvents.filter( @@ -88,9 +85,7 @@ test.describe('network signals - XHR', () => { }) // Wait for the signals to be flushed - await indexPage.waitForSignalsApiFlush() - - const networkEvents = indexPage.signalsAPI.getEvents('network') + const networkEvents = await indexPage.signalsAPI.waitForEvents(2, 'network') // Check the request const requests = networkEvents.filter( @@ -130,9 +125,8 @@ test.describe('network signals - XHR', () => { }) // Wait for the signals to be flushed - await indexPage.waitForSignalsApiFlush() - const networkEvents = indexPage.signalsAPI.getEvents('network') + const networkEvents = await indexPage.signalsAPI.waitForEvents(1, 'network') // Check the response (only response should be captured) const responses = networkEvents.filter( @@ -163,7 +157,7 @@ test.describe('network signals - XHR', () => { }) // Wait for the signals to be flushed - await indexPage.waitForSignalsApiFlush() + await indexPage.signalsAPI.waitForEvents(1, 'network') // Retrieve the batch of events from the signals request const networkEvents = indexPage.signalsAPI.getEvents('network') @@ -211,10 +205,8 @@ test.describe('network signals - XHR', () => { ]) // Wait for the signals to be flushed - await indexPage.waitForSignalsApiFlush() + const networkEvents = await indexPage.signalsAPI.waitForEvents(2, 'network') - // Retrieve the batch of events from the signals request - const networkEvents = indexPage.signalsAPI.getEvents('network') // Check the request const requests = networkEvents.filter( (el) => el.properties!.data.action === 'request' @@ -252,7 +244,7 @@ test.describe('network signals - XHR', () => { contentType: 'application/json', }) - await indexPage.waitForSignalsApiFlush() + await indexPage.signalsAPI.waitForEvents(2, 'network') const networkEvents = indexPage.signalsAPI.getEvents('network') @@ -290,7 +282,7 @@ test.describe('network signals - XHR', () => { contentType: 'application/json', }) - await indexPage.waitForSignalsApiFlush() + await indexPage.signalsAPI.waitForEvents(2, 'network') const networkEvents = indexPage.signalsAPI.getEvents('network') @@ -331,8 +323,7 @@ test.describe('network signals - XHR', () => { responseType: 'json', contentType: 'application/json', }) - - await indexPage.waitForSignalsApiFlush() + await indexPage.signalsAPI.waitForEvents(2, 'network') const networkEvents = indexPage.signalsAPI.getEvents('network') 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 new file mode 100644 index 000000000..c0ea50242 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/runtime-constants.test.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' +import { IndexPage } from './index-page' + +const basicEdgeFn = ` + const processSignal = (signal) => { + // test that constants are properly injected + if (typeof EventType !== 'object') { + throw new Error('EventType is missing?') + } + if (typeof SignalType !== 'object') { + throw new Error('SignalType is missing?') + } + if (typeof NavigationAction !== 'object') { + throw new Error('NavigationAction is missing?') + } + + if (signal.type === SignalType.Interaction) { + const eventName = signal.data.eventType + ' ' + '[' + signal.type + ']' + analytics.track(eventName, signal.data) + } + }` + +let indexPage: IndexPage + +test.beforeEach(async ({ page }) => { + indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn) +}) + +test('constants should be accessible in the runtime', async () => { + /** + * Make a button click, see ifdom.window.NavigationAction.URLChange it: + * - creates an interaction signal that sends to the signals endpoint + * - creates an analytics event that sends to the tracking endpoint + */ + await Promise.all([ + indexPage.clickButton(), + indexPage.waitForTrackingApiFlush(), + ]) + + const analyticsReqJSON = indexPage.trackingAPI.lastEvent() + expect(analyticsReqJSON).toMatchObject({ + event: 'click [interaction]', + }) +}) 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 3dc8b67e4..a03d82b42 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,14 +2,12 @@ import { test, expect } from '@playwright/test' import { waitForCondition } from '../../helpers/playwright-utils' import { IndexPage } from './index-page' -const indexPage = new IndexPage() - const basicEdgeFn = `const processSignal = (signal) => {}` test('redaction enabled -> will XXX the value of text input', async ({ page, }) => { - await indexPage.loadAndWait(page, basicEdgeFn, { + const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { disableSignalsRedaction: false, enableSignalsIngestion: true, }) @@ -39,11 +37,10 @@ test('redaction enabled -> will XXX the value of text input', async ({ test('redation disabled -> will not touch the value of text input', async ({ page, }) => { - await indexPage.loadAndWait(page, basicEdgeFn, { + const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { disableSignalsRedaction: true, enableSignalsIngestion: true, }) - await Promise.all([ indexPage.fillNameInput('John Doe'), indexPage.waitForSignalsApiFlush(), diff --git a/packages/signals/signals-runtime/.eslintrc.js b/packages/signals/signals-runtime/.eslintrc.js new file mode 100644 index 000000000..eedf2f3c3 --- /dev/null +++ b/packages/signals/signals-runtime/.eslintrc.js @@ -0,0 +1,7 @@ +/** @type { import('eslint').Linter.Config } */ +module.exports = { + extends: ['../../../.eslintrc'], + env: { + browser: true, + }, +} diff --git a/packages/signals/signals-runtime/.lintstagedrc.js b/packages/signals/signals-runtime/.lintstagedrc.js new file mode 100644 index 000000000..bc1f1c780 --- /dev/null +++ b/packages/signals/signals-runtime/.lintstagedrc.js @@ -0,0 +1 @@ +module.exports = require("@internal/config").lintStagedConfig diff --git a/packages/signals/signals-runtime/LICENSE b/packages/signals/signals-runtime/LICENSE new file mode 100644 index 000000000..a0378adfd --- /dev/null +++ b/packages/signals/signals-runtime/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2023 Segment + +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. diff --git a/packages/signals/signals-runtime/README.md b/packages/signals/signals-runtime/README.md new file mode 100644 index 000000000..4c676f88e --- /dev/null +++ b/packages/signals/signals-runtime/README.md @@ -0,0 +1,10 @@ +# @segment/analytics-signals-runtime +Encapsults Signals runtime functionality, in order to share logic between the signals plugins for browser and mobile. + +### Development +`yarn build` generate the following artifacts: +| Generated File(s) | Description | +|--------|-------------| +| `/dist/runtime/index.[platform].js`, `/[platform]/get-runtime-code.generated.js` | Exposes `globalThis.Signals` and constants (e.g. `SignalType`), either through the script tag or in the mobile JS engine | +| `/dist/editor/[platform]-editor.d.ts.txt` | Type definitions for monaco editor on app.segment.com +| `/dist/esm/index.js` | Entry point for `@segment/analytics-signals` and the segment app, that want to consume the types / runtime code. | \ No newline at end of file diff --git a/packages/signals/signals-runtime/api-extractor.base.json b/packages/signals/signals-runtime/api-extractor.base.json new file mode 100644 index 000000000..ff771d28d --- /dev/null +++ b/packages/signals/signals-runtime/api-extractor.base.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "compiler": { + "tsconfigFilePath": "./tsconfig.build.json" + }, + "messages": { + "compilerMessageReporting": { + "default": { + "logLevel": "none" + } + }, + "extractorMessageReporting": { + "default": { + "logLevel": "none" + }, + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-internal-missing-underscore": { + "logLevel": "none" + }, + "ae-forgotten-export": { + "logLevel": "none" + } + } + }, + "apiReport": { + "enabled": false + }, + "docModel": { + "enabled": false + } +} diff --git a/packages/signals/signals-runtime/api-extractor.mobile.json b/packages/signals/signals-runtime/api-extractor.mobile.json new file mode 100644 index 000000000..2fdf092dc --- /dev/null +++ b/packages/signals/signals-runtime/api-extractor.mobile.json @@ -0,0 +1,8 @@ +{ + "extends": "./api-extractor.base.json", + "mainEntryPointFilePath": "./dist/types/mobile/index.mobile-editor.d.ts", + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "./dist/editor/mobile-editor.d.ts.txt" + } +} diff --git a/packages/signals/signals-runtime/api-extractor.web.json b/packages/signals/signals-runtime/api-extractor.web.json new file mode 100644 index 000000000..84d46b927 --- /dev/null +++ b/packages/signals/signals-runtime/api-extractor.web.json @@ -0,0 +1,8 @@ +{ + "extends": "./api-extractor.base.json", + "mainEntryPointFilePath": "./dist/types/web/index.web-editor.d.ts", + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "./dist/editor/web-editor.d.ts.txt" + } +} diff --git a/packages/signals/signals-runtime/build-editor-types.js b/packages/signals/signals-runtime/build-editor-types.js new file mode 100644 index 000000000..a88664590 --- /dev/null +++ b/packages/signals/signals-runtime/build-editor-types.js @@ -0,0 +1,48 @@ +/* eslint-disable no-undef */ +const { execSync } = require('child_process') +const fsPromises = require('fs').promises +const pkgJSON = require('./package.json') +async function prependGenerated(filePath) { + const content = [ + '/* eslint-disable */', + '// These types will be used in the segment app UI for autocomplete', + `// Generated from: ${pkgJSON.name}@${pkgJSON.version}`, + '// Built as text so they do not cause global scope pollution for packages importing unrelated modules', + '\n', + ].join('\n') + try { + const fileContent = await fsPromises.readFile(filePath, 'utf8') + await fsPromises.writeFile(filePath, content + fileContent) + } catch (error) { + console.error(`Error prepending generated content: ${error}`) + } +} + +async function removeExports(filePath) { + try { + const data = await fsPromises.readFile(filePath, 'utf8') + // Remove 'export { }' lines and any instance of the word 'export' + let result = data + .split('\n') + .filter((line) => line.trim() !== 'export { }') // Remove lines that are exactly 'export { }' + .map((line) => line.replace(/\bexport\b/g, '')) // Remove any instance of the word 'export' + .join('\n') + await fsPromises.writeFile(filePath, result, 'utf8') + } catch (error) { + console.error(`Error exports: ${error}`) + } +} + +const main = async () => { + execSync('yarn build:esm', { stdio: 'inherit' }) + execSync('npx api-extractor run --config ./api-extractor.mobile.json --local') + execSync('npx api-extractor run --config ./api-extractor.web.json --local') + const outputs = [ + './dist/editor/web-editor.d.ts.txt', + './dist/editor/mobile-editor.d.ts.txt', + ] + await Promise.all(outputs.map(removeExports)) + await Promise.all(outputs.map(prependGenerated)) + console.log('wrote:', outputs.join(', ')) +} +main() diff --git a/packages/signals/signals-runtime/build-signals-runtime-global.js b/packages/signals/signals-runtime/build-signals-runtime-global.js new file mode 100644 index 000000000..924b1aa6a --- /dev/null +++ b/packages/signals/signals-runtime/build-signals-runtime-global.js @@ -0,0 +1,106 @@ +const esbuild = require('esbuild') +const path = require('path') +const fs = require('fs') +const fsPromises = fs.promises +const pkgJSON = require('./package.json') + +const getBanner = (entryPoint) => { + const content = [ + `// GENERATED, DO NOT EDIT`, + `// ${pkgJSON.name}@${pkgJSON.version}`, + `// Entry point: ${entryPoint}`, + ].join('\n') + return content +} + +/** + * + * @param {"web" | "mobile"} platform + */ +const getEntryPoint = (platform) => { + return `src/${platform}/index.signals-runtime.ts` +} + +const getOutFiles = (platform) => { + const outfileMinified = `./dist/runtime/index.${platform}.min.js` + const outfileUnminified = `./dist/runtime/index.${platform}.js` + return { + outfileMinified, + outfileUnminified, + } +} + +async function prependContent(filePath, content) { + try { + const fileContent = await fs.promises.readFile(filePath, 'utf8') + await fsPromises.writeFile(filePath, content + '\n' + fileContent) + } catch (error) { + console.error(`Error prepending generated content: ${error}`) + } +} + +const buildRuntimeAsString = async (platform) => { + const banner = getBanner(getEntryPoint(platform)) + const { outfileMinified } = getOutFiles(platform) + + const runtimeContent = fs + .readFileSync(outfileMinified, 'utf-8') + .replace(banner, '') // remove existing banner from code section to be added back later + const generatedDir = path.resolve(__dirname, `src/${platform}`) + if (!fs.existsSync(generatedDir)) { + fs.mkdirSync(generatedDir, { recursive: true }) + } + + const generatedTsFile = `./src/${platform}/get-runtime-code.generated.ts` + const tsFileContent = `export const getRuntimeCode = (): string => \`${runtimeContent}\` + ` + + fs.writeFileSync(generatedTsFile, tsFileContent) + + // add back banner + await prependContent( + generatedTsFile, + ['/* eslint-disable */', banner].join('\n') + ) + console.log(`wrote: ${generatedTsFile}`) +} + +const buildRuntime = async (platform) => { + const entryPoint = getEntryPoint(platform) + const { outfileUnminified, outfileMinified } = getOutFiles(platform) + + // Build minified version + await esbuild.build({ + entryPoints: [entryPoint], + outfile: outfileMinified, + bundle: true, + minify: true, + banner: { js: getBanner(entryPoint) }, + }) + console.log(`wrote: ${outfileMinified}`) + + // Build unminified version + await esbuild.build({ + entryPoints: [entryPoint], + outfile: outfileUnminified, + bundle: true, + minify: false, + banner: { js: getBanner(entryPoint) }, + }) + console.log(`wrote: ${outfileUnminified}`) +} + +const buildAll = async () => { + const PLATFORMS = ['web', 'mobile'] + for (const platform of PLATFORMS) { + try { + await buildRuntime(platform) + await buildRuntimeAsString(platform) + } catch (err) { + console.error(err) + process.exit(1) + } + } +} + +buildAll() diff --git a/packages/signals/signals-runtime/bundle-build-tests/runtime-env.test.ts b/packages/signals/signals-runtime/bundle-build-tests/runtime-env.test.ts new file mode 100644 index 000000000..3e72e5a8c --- /dev/null +++ b/packages/signals/signals-runtime/bundle-build-tests/runtime-env.test.ts @@ -0,0 +1,69 @@ +import { JSDOM } from 'jsdom' +import fs from 'node:fs' +import path from 'node:path' + +describe('Global Scope Test: Web', () => { + let dom: JSDOM + beforeAll(() => { + // Load the built file + const filePath = path.resolve(__dirname, '../dist/runtime/index.web.js') + const scriptContent = fs.readFileSync(filePath, 'utf-8') + + // Create a new JSDOM instance + dom = new JSDOM(``, { + runScripts: 'dangerously', + resources: 'usable', + }) + + // Execute the script in the JSDOM context + const scriptElement = dom.window.document.createElement('script') + scriptElement.textContent = scriptContent + dom.window.document.head.appendChild(scriptElement) + }) + + test('should expose a signals instance in the global scope', () => { + // @ts-ignore + expect(dom.window).toBeDefined() + // @ts-ignore + expect(typeof dom.window.signals).toBe('object') + }) + + test('should expose constants', () => { + expect(dom.window.EventType.Track).toBe('track') + expect(dom.window.NavigationAction.URLChange).toBe('urlChange') + expect(dom.window.SignalType.Interaction).toBe('interaction') + }) +}) + +describe('Global Scope Test: Mobile', () => { + let dom: JSDOM + beforeAll(() => { + // Load the built file + const filePath = path.resolve(__dirname, '../dist/runtime/index.mobile.js') + const scriptContent = fs.readFileSync(filePath, 'utf-8') + + // Create a new JSDOM instance + dom = new JSDOM(``, { + runScripts: 'dangerously', + resources: 'usable', + }) + + // Execute the script in the JSDOM context + const scriptElement = dom.window.document.createElement('script') + scriptElement.textContent = scriptContent + dom.window.document.head.appendChild(scriptElement) + }) + + test('should expose signals in the global scope', () => { + // @ts-ignore + expect(dom.window).toBeDefined() + // @ts-ignore + expect(typeof dom.window.signals).toBe('object') + }) + + test('should expose constants', () => { + expect(dom.window.EventType.Track).toBe('track') + expect(dom.window.NavigationAction.Forward).toBe('forward') + expect(dom.window.SignalType.Interaction).toBe('interaction') + }) +}) diff --git a/packages/signals/signals-runtime/jest.config.js b/packages/signals/signals-runtime/jest.config.js new file mode 100644 index 000000000..824e08ca5 --- /dev/null +++ b/packages/signals/signals-runtime/jest.config.js @@ -0,0 +1,6 @@ +const { createJestTSConfig } = require('@internal/config') + +module.exports = createJestTSConfig(__dirname, { + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['./jest.setup.js'], +}) diff --git a/packages/signals/signals-runtime/jest.setup.js b/packages/signals/signals-runtime/jest.setup.js new file mode 100644 index 000000000..65d0a93b2 --- /dev/null +++ b/packages/signals/signals-runtime/jest.setup.js @@ -0,0 +1,9 @@ +const fetch = require('node-fetch') +const { TextEncoder, TextDecoder } = require('util') + +// fix: "ReferenceError: TextEncoder is not defined" after upgrading JSDOM +global.TextEncoder = TextEncoder +global.TextDecoder = TextDecoder + +// eslint-disable-next-line no-undef +globalThis.fetch = fetch // polyfill fetch so nock will work correctly on node 18 (https://github.com/nock/nock/issues/2336) diff --git a/packages/signals/signals-runtime/package.json b/packages/signals/signals-runtime/package.json new file mode 100644 index 000000000..0a09ee152 --- /dev/null +++ b/packages/signals/signals-runtime/package.json @@ -0,0 +1,49 @@ +{ + "name": "@segment/analytics-signals-runtime", + "version": "0.0.0", + "keywords": [ + "segment" + ], + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "LICENSE", + "dist/", + "src/", + "!**/__tests__/**", + "!**/test-helpers/**", + "!*.tsbuildinfo" + ], + "scripts": { + ".": "yarn run -T turbo run --filter=@segment/analytics-signals-runtime...", + "test": "yarn jest", + "lint": "yarn concurrently 'yarn:eslint .' 'yarn:tsc --noEmit'", + "build": "rm -rf dist && yarn concurrently 'yarn:build:*'", + "build:editor": "node build-editor-types.js", + "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", + "watch": "yarn build:esm --watch", + "watch:test": "yarn test --watch", + "tsc": "yarn run -T tsc", + "eslint": "yarn run -T eslint", + "concurrently": "yarn run -T concurrently", + "jest": "yarn run -T jest", + "webpack": "yarn run -T webpack" + }, + "dependencies": { + "tslib": "^2.4.1" + }, + "packageManager": "yarn@3.4.1", + "license": "MIT", + "repository": { + "directory": "packages/signals/signals-runtime", + "type": "git", + "url": "https://github.com/segmentio/analytics-next" + }, + "devDependencies": { + "@internal/test-helpers": "workspace:^", + "@microsoft/api-extractor": "^7.47.9" + } +} diff --git a/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts b/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts new file mode 100644 index 000000000..f9d8f243e --- /dev/null +++ b/packages/signals/signals-runtime/src/__tests__/signals-runtime.test.ts @@ -0,0 +1,111 @@ +import { InstrumentationSignal, InteractionSignal, Signal } from '../index' +import { + mockInstrumentationSignal, + mockInteractionSignal, +} from '../test-helpers/mocks/mock-signal-types-web' + +import { WebSignalsRuntime } from '../web/web-signals-runtime' +describe(WebSignalsRuntime, () => { + let signalsRuntime: WebSignalsRuntime + let signal1: InstrumentationSignal + let signal2: InteractionSignal + let signal3: InteractionSignal + let mockSignals: Signal[] = [] + beforeEach(() => { + signal1 = mockInstrumentationSignal + signal2 = mockInteractionSignal + signal3 = { + ...mockInteractionSignal, + data: { eventType: 'change', target: {} }, + } + mockSignals = [signal1, signal2, signal3] + signalsRuntime = new WebSignalsRuntime(mockSignals) + }) + + describe('meta', () => { + it('should be serializable / iterable', () => { + const copy = { ...signalsRuntime } + expect(copy).toMatchObject({ + find: expect.any(Function), + filter: expect.any(Function), + }) + }) + }) + + describe('find', () => { + it('should find following signal based on the provided function', () => { + const result = signalsRuntime.find(signal2, 'interaction', () => true) + expect(result).toEqual(signal3) + }) + + it('should return undefined if there are no following signals that match the query', () => { + const result = signalsRuntime.find(signal1, 'instrumentation', () => true) + expect(result).toEqual(undefined) + }) + + it('should filter based on predicate', () => { + const result = signalsRuntime.find( + signal1, + 'interaction', + (signal) => signal.data.eventType === 'change' + ) + expect(result).toEqual(signal3) + }) + + it('should return the first match if predicate is not provided', () => { + const result = signalsRuntime.find(signal1, 'interaction') + expect(result).toEqual(signal2) + }) + }) + + describe('filter', () => { + it('should filter based on the provided function', () => { + const result = signalsRuntime.filter(signal2, 'interaction', () => true) + expect(result).toEqual([signal3]) + }) + + it('should return an empty array if there are no following signals that match the query', () => { + const result = signalsRuntime.filter( + signal1, + 'instrumentation', + () => true + ) + expect(result).toEqual([]) + }) + + it('should filter based on predicate', () => { + const result = signalsRuntime.filter( + signal1, + 'interaction', + (signal) => signal.data.eventType === 'change' + ) + expect(result).toEqual([signal3]) + }) + + it('should return all matches if predicate is not provided', () => { + const result = signalsRuntime.filter(signal1, 'interaction') + expect(result).toEqual([signal2, signal3]) + }) + }) + + describe('signalBuffer property', () => { + beforeEach(() => { + signalsRuntime = new WebSignalsRuntime() + }) + it('should have no signals by default', () => { + expect(signalsRuntime.signalBuffer).toEqual([]) + }) + it('should let you set the signal buffer', () => { + signalsRuntime = new WebSignalsRuntime() + const newSignal = mockInstrumentationSignal + signalsRuntime.signalBuffer = [newSignal] + expect(signalsRuntime.signalBuffer).toEqual([newSignal]) + }) + + it('should support find, etc', () => { + signalsRuntime.signalBuffer = mockSignals + const result = signalsRuntime.find(signal2, 'interaction', () => true) + expect(result).toEqual(signal3) + }) + }) +}) diff --git a/packages/signals/signals-runtime/src/index.ts b/packages/signals/signals-runtime/src/index.ts new file mode 100644 index 000000000..7c4148ac7 --- /dev/null +++ b/packages/signals/signals-runtime/src/index.ts @@ -0,0 +1,17 @@ +// This file is only for people who plan on installing this package in their npm projects, like us. + +// shared +export { SignalsRuntime } from './shared/signals-runtime' + +// web +export * from './web/web-signals-types' +export * from './shared/shared-types' +export * as WebRuntimeConstants from './web/web-constants' +export { getRuntimeCode } from './web/get-runtime-code.generated' +export { WebSignalsRuntime } from './web/web-signals-runtime' + +// mobile -- we don't need this *yet*, but some day? +export * as Mobile from './mobile/mobile-signals-types' +export * as MobileRuntimeConstants from './mobile/mobile-constants' +export { MobileSignalsRuntime } from './mobile/mobile-signals-runtime' +export { getRuntimeCode as getMobileRuntimeCode } from './mobile/get-runtime-code.generated' diff --git a/packages/signals/signals-runtime/src/mobile/get-runtime-code.generated.ts b/packages/signals/signals-runtime/src/mobile/get-runtime-code.generated.ts new file mode 100644 index 000000000..1269f6cce --- /dev/null +++ b/packages/signals/signals-runtime/src/mobile/get-runtime-code.generated.ts @@ -0,0 +1,8 @@ +/* eslint-disable */ +// GENERATED, DO NOT EDIT +// @segment/analytics-signals-runtime@0.0.0 +// Entry point: src/mobile/index.signals-runtime.ts +export const getRuntimeCode = (): string => ` +"use strict";(()=>{var g=Object.defineProperty;var f=(r,n)=>{for(var e in n)g(r,e,{get:n[e],enumerable:!0})};var l={};f(l,{EventType:()=>d,LocalDataAction:()=>c,NavigationAction:()=>S,NetworkAction:()=>u,SignalType:()=>p});var p=Object.freeze({Interaction:"interaction",Navigation:"navigation",Network:"network",LocalData:"localData",Instrumentation:"instrumentation",UserDefined:"userDefined"}),d=Object.freeze({Track:"track",Page:"page",Screen:"screen",Identify:"identify",Group:"group",Alias:"alias"}),S=Object.freeze({Forward:"forward",Backward:"backward",Modal:"modal",Entering:"entering",Leaving:"leaving",Page:"page",Popup:"popup"}),u=Object.freeze({Request:"request",Response:"response"}),c=Object.freeze({Loaded:"loaded",Updated:"updated",Saved:"saved",Deleted:"deleted",Undefined:"undefined"});var a=class{constructor(n=[]){this.find=(n,e,i)=>this.filter(n,e,i)[0];this.filter=(n,e,i)=>{let o=s=>s.type===e;return this.signalBuffer.slice(this.signalBuffer.indexOf(n)+1).filter(o).filter(s=>i?i(s):()=>!0)};this.signalBuffer=n}};var t=class extends a{constructor(e=[]){super(e);this.add=e=>{this.signalCounter<0&&(this.signalCounter=0),"index"in e&&e.index==-1&&(e.index=this.getNextIndex()),this.signalBuffer.unshift(e),this.signalBuffer.length>this.maxBufferSize&&this.signalBuffer.pop()};this.getNextIndex=()=>{let e=this.signalCounter;return this.signalCounter+=1,e};this.signalCounter=0,this.maxBufferSize=1e3}};Object.assign(globalThis,{signals:new t},l);})(); +` + \ No newline at end of file diff --git a/packages/signals/signals-runtime/src/mobile/index.mobile-editor.ts b/packages/signals/signals-runtime/src/mobile/index.mobile-editor.ts new file mode 100644 index 000000000..c3e5d72a4 --- /dev/null +++ b/packages/signals/signals-runtime/src/mobile/index.mobile-editor.ts @@ -0,0 +1,11 @@ +import { MobileSignalsRuntime } from './mobile-signals-runtime' +export { MobileSignalsRuntime } + +export const signals = new MobileSignalsRuntime() + +/** + * Entry point for the editor definitions + */ +export * from './mobile-signals-types' +export * from '../shared/shared-types' +export * from './mobile-constants' diff --git a/packages/signals/signals-runtime/src/mobile/index.signals-runtime.ts b/packages/signals/signals-runtime/src/mobile/index.signals-runtime.ts new file mode 100644 index 000000000..8d5373eee --- /dev/null +++ b/packages/signals/signals-runtime/src/mobile/index.signals-runtime.ts @@ -0,0 +1,13 @@ +import * as Constants from './mobile-constants' +import { MobileSignalsRuntime } from './mobile-signals-runtime' + +// assign SignalsRuntime and all constants to globalThis +// meant to replace this: +// https://github.com/segmentio/SignalsJS-Runtime/blob/main/Runtime/Signals.js +Object.assign( + globalThis, + { + signals: new MobileSignalsRuntime(), + }, + Constants +) diff --git a/packages/signals/signals-runtime/src/mobile/mobile-constants.ts b/packages/signals/signals-runtime/src/mobile/mobile-constants.ts new file mode 100644 index 000000000..48e6665ec --- /dev/null +++ b/packages/signals/signals-runtime/src/mobile/mobile-constants.ts @@ -0,0 +1,40 @@ +export const SignalType = Object.freeze({ + Interaction: 'interaction', + Navigation: 'navigation', + Network: 'network', + LocalData: 'localData', + Instrumentation: 'instrumentation', + UserDefined: 'userDefined', +}) + +export const EventType = Object.freeze({ + Track: 'track', + Page: 'page', + Screen: 'screen', + Identify: 'identify', + Group: 'group', + Alias: 'alias', +}) + +export const NavigationAction = Object.freeze({ + Forward: 'forward', + Backward: 'backward', + Modal: 'modal', + Entering: 'entering', + Leaving: 'leaving', + Page: 'page', + Popup: 'popup', +}) + +export const NetworkAction = Object.freeze({ + Request: 'request', + Response: 'response', +}) + +export const LocalDataAction = Object.freeze({ + Loaded: 'loaded', + Updated: 'updated', + Saved: 'saved', + Deleted: 'deleted', + Undefined: 'undefined', +}) diff --git a/packages/signals/signals-runtime/src/mobile/mobile-signals-runtime.ts b/packages/signals/signals-runtime/src/mobile/mobile-signals-runtime.ts new file mode 100644 index 000000000..0ec4b1d49 --- /dev/null +++ b/packages/signals/signals-runtime/src/mobile/mobile-signals-runtime.ts @@ -0,0 +1,33 @@ +import { SignalsRuntime } from '../shared/signals-runtime' +import { Signal } from './mobile-signals-types' + +export class MobileSignalsRuntime extends SignalsRuntime { + private signalCounter: number + private maxBufferSize: number + constructor(signals: Signal[] = []) { + super(signals) + this.signalCounter = 0 + this.maxBufferSize = 1000 + } + add = (signal: Signal) => { + if (this.signalCounter < 0) { + this.signalCounter = 0 + } + + if ('index' in signal && signal.index == -1) { + // this was previously broken for ages, not sure when this code path would ever be used. + // My understanding is that currently, getNextIndex() is called _outside_ of this function and used to construct the added signal. - seth + signal.index = this.getNextIndex() + } + this.signalBuffer.unshift(signal) + if (this.signalBuffer.length > this.maxBufferSize) { + this.signalBuffer.pop() + } + } + + getNextIndex = () => { + const index = this.signalCounter + this.signalCounter += 1 + return index + } +} diff --git a/packages/signals/signals-runtime/src/mobile/mobile-signals-types.ts b/packages/signals/signals-runtime/src/mobile/mobile-signals-types.ts new file mode 100644 index 000000000..ec9544796 --- /dev/null +++ b/packages/signals/signals-runtime/src/mobile/mobile-signals-types.ts @@ -0,0 +1,90 @@ +import { BaseSignal } from '../shared/shared-types' + +export type SignalTypes = Signal['type'] + +export type NavigationActionName = + | 'forward' + | 'backward' + | 'modal' + | 'entering' + | 'leaving' + | 'page' + | 'popup' + +export type NetworkActionName = 'request' | 'response' + +export type LocalDataActionName = + | 'loaded' + | 'updated' + | 'saved' + | 'deleted' + | 'undefined' + +export type Signal = + | InteractionSignal + | NavigationSignal + | NetworkSignal + | LocalDataSignal + | InstrumentationSignal + | UserDefinedSignal + +interface RawSignal extends BaseSignal { + type: SignalType + anonymousId: string + data: any + timestamp: string + index: any +} + +interface NavigationData { + action: NavigationActionName + screen: string +} + +interface NavigationSignal extends RawSignal<'navigation'> { + data: NavigationData +} + +interface InteractionData { + component: string + info: string + data: any +} + +interface InteractionSignal extends RawSignal<'interaction'> { + type: 'interaction' + data: InteractionData +} + +interface NetworkData { + action: NetworkActionName + url: string + data: any +} + +interface NetworkSignal extends RawSignal<'network'> { + data: NetworkData +} + +interface LocalData { + action: LocalDataActionName + identifier: string + data: string +} + +interface LocalDataSignal extends RawSignal<'localData'> { + data: LocalData +} + +interface UserDefinedSignal extends RawSignal<'userDefined'> { + data: any +} + +interface InstrumentationData { + type: 'instrumentation' + rawEvent: any +} + +interface InstrumentationSignal extends RawSignal<'instrumentation'> { + data: InstrumentationData +} diff --git a/packages/signals/signals-runtime/src/shared/shared-types.ts b/packages/signals/signals-runtime/src/shared/shared-types.ts new file mode 100644 index 000000000..afdf57786 --- /dev/null +++ b/packages/signals/signals-runtime/src/shared/shared-types.ts @@ -0,0 +1,21 @@ +export interface BaseSignal { + type: string +} + +export type SignalOfType< + AllSignals extends BaseSignal, + SignalType extends AllSignals['type'] +> = AllSignals & { type: SignalType } + +export type JSONPrimitive = string | number | boolean | null +export type JSONValue = JSONPrimitive | JSONObject | JSONArray +export type JSONObject = { [member: string]: JSONValue } +export type JSONArray = JSONValue[] + +export interface SegmentEvent { + /** + * @example 'track' | 'page' | 'screen' | 'identify' | 'group' | 'alias' + */ + type: string + [key: string]: unknown +} diff --git a/packages/signals/signals-runtime/src/shared/signals-runtime.ts b/packages/signals/signals-runtime/src/shared/signals-runtime.ts new file mode 100644 index 000000000..513205fbb --- /dev/null +++ b/packages/signals/signals-runtime/src/shared/signals-runtime.ts @@ -0,0 +1,52 @@ +import { BaseSignal, SignalOfType } from '../shared/shared-types' + +/** + * Base class that provides runtime utilities for signals. + */ +export abstract class SignalsRuntime { + signalBuffer: Signal[] + + constructor(signals: Signal[] = []) { + // initial signals + this.signalBuffer = signals + } + + /** + * Finds a signal of a specific type from a given signal. + * + * SignalType - The type of the signal to find. + * @param fromSignal - The signal to search from. + * @param signalType - The type of the signal to find. + * @param predicate - Optional predicate function to filter the signals. + * @returns The found signal of the specified type, or undefined if not found. + */ + find = ( + fromSignal: Signal, + signalType: SignalType, + predicate?: (signal: SignalOfType) => boolean + ): SignalOfType | undefined => { + return this.filter(fromSignal, signalType, predicate)[0] + } + + /** + * Filters signals of a specific type from a given signal. + * SignalType - The type of the signals to filter. + * @param fromSignal - The signal to search from. + * @param signalType - The type of the signals to filter. + * @param predicate - Optional predicate function to filter the signals. + * @returns An array of signals of the specified type. + */ + filter = ( + fromSignal: Signal, + signalType: SignalType, + predicate?: (signal: SignalOfType) => boolean + ): SignalOfType[] => { + const _isSignalOfType = ( + signal: Signal + ): signal is SignalOfType => signal.type === signalType + return this.signalBuffer + .slice(this.signalBuffer.indexOf(fromSignal) + 1) + .filter(_isSignalOfType) + .filter((signal) => (predicate ? predicate(signal) : () => true)) + } +} diff --git a/packages/signals/signals-runtime/src/test-helpers/mocks/mock-signal-types-web.ts b/packages/signals/signals-runtime/src/test-helpers/mocks/mock-signal-types-web.ts new file mode 100644 index 000000000..8ca888438 --- /dev/null +++ b/packages/signals/signals-runtime/src/test-helpers/mocks/mock-signal-types-web.ts @@ -0,0 +1,54 @@ +import { + InteractionSignal, + NavigationSignal, + InstrumentationSignal, + UserDefinedSignal, + NetworkSignal, +} from '../../web/web-signals-types' +// Mock data for testing +export const mockInteractionSignal: InteractionSignal = { + type: 'interaction', + data: { + eventType: 'click', + target: { id: 'button1', className: 'btn-primary' }, + }, + metadata: { timestamp: Date.now() }, +} + +export const mockNavigationSignal: NavigationSignal = { + type: 'navigation', + data: { + action: 'urlChange', + url: 'https://example.com', + hash: '#section1', + prevUrl: 'https://example.com/home', + }, + metadata: { timestamp: Date.now() }, +} + +export const mockInstrumentationSignal: InstrumentationSignal = { + type: 'instrumentation', + data: { + rawEvent: { type: 'customEvent', detail: 'example' }, + }, + metadata: { timestamp: Date.now() }, +} + +export const mockNetworkSignal: NetworkSignal = { + type: 'network', + data: { + action: 'request', + url: 'https://api.example.com/data', + method: 'GET', + data: { key: 'value' }, + }, + metadata: { timestamp: Date.now() }, +} + +export const mockUserDefinedSignal: UserDefinedSignal = { + type: 'userDefined', + data: { + customField: 'customValue', + }, + metadata: { timestamp: Date.now() }, +} diff --git a/packages/signals/signals-runtime/src/web/get-runtime-code.generated.ts b/packages/signals/signals-runtime/src/web/get-runtime-code.generated.ts new file mode 100644 index 000000000..4263262c9 --- /dev/null +++ b/packages/signals/signals-runtime/src/web/get-runtime-code.generated.ts @@ -0,0 +1,8 @@ +/* eslint-disable */ +// GENERATED, DO NOT EDIT +// @segment/analytics-signals-runtime@0.0.0 +// Entry point: src/web/index.signals-runtime.ts +export const getRuntimeCode = (): string => ` +"use strict";(()=>{var o=Object.defineProperty;var S=(l,e)=>{for(var n in e)o(l,n,{get:e[n],enumerable:!0})};var i=class{constructor(e=[]){this.find=(e,n,a)=>this.filter(e,n,a)[0];this.filter=(e,n,a)=>{let s=g=>g.type===n;return this.signalBuffer.slice(this.signalBuffer.indexOf(e)+1).filter(s).filter(g=>a?a(g):()=>!0)};this.signalBuffer=e}};var t=class extends i{};var r={};S(r,{EventType:()=>f,NavigationAction:()=>p,SignalType:()=>y});var f=Object.freeze({Track:"track",Page:"page",Screen:"screen",Identify:"identify",Group:"group",Alias:"alias"}),p=Object.freeze({URLChange:"urlChange",PageLoad:"pageLoad"}),y=Object.freeze({Interaction:"interaction",Navigation:"navigation",Network:"network",LocalData:"localData",Instrumentation:"instrumentation",UserDefined:"userDefined"});Object.assign(globalThis,{signals:new t},r);})(); +` + \ No newline at end of file diff --git a/packages/signals/signals-runtime/src/web/index.signals-runtime.ts b/packages/signals/signals-runtime/src/web/index.signals-runtime.ts new file mode 100644 index 000000000..ec958140e --- /dev/null +++ b/packages/signals/signals-runtime/src/web/index.signals-runtime.ts @@ -0,0 +1,11 @@ +import { WebSignalsRuntime } from './web-signals-runtime' +import * as Constants from './web-constants' + +// assign SignalsRuntime and all constants to globalThis +Object.assign( + globalThis, + { + signals: new WebSignalsRuntime(), + }, + Constants +) diff --git a/packages/signals/signals-runtime/src/web/index.web-editor.ts b/packages/signals/signals-runtime/src/web/index.web-editor.ts new file mode 100644 index 000000000..19ab4ab7e --- /dev/null +++ b/packages/signals/signals-runtime/src/web/index.web-editor.ts @@ -0,0 +1,11 @@ +import { WebSignalsRuntime } from './web-signals-runtime' +export { WebSignalsRuntime } + +export const signals = new WebSignalsRuntime() + +/** + * Entry point for the editor definitions + */ +export * from './web-signals-types' +export * from '../shared/shared-types' +export * from './web-constants' diff --git a/packages/signals/signals-runtime/src/web/web-constants.ts b/packages/signals/signals-runtime/src/web/web-constants.ts new file mode 100644 index 000000000..702dc5185 --- /dev/null +++ b/packages/signals/signals-runtime/src/web/web-constants.ts @@ -0,0 +1,22 @@ +export const EventType = Object.freeze({ + Track: 'track', + Page: 'page', + Screen: 'screen', + Identify: 'identify', + Group: 'group', + Alias: 'alias', +}) + +export const NavigationAction = Object.freeze({ + URLChange: 'urlChange', + PageLoad: 'pageLoad', +}) + +export const SignalType = Object.freeze({ + Interaction: 'interaction', + Navigation: 'navigation', + Network: 'network', + LocalData: 'localData', + Instrumentation: 'instrumentation', + UserDefined: 'userDefined', +}) diff --git a/packages/signals/signals-runtime/src/web/web-signals-runtime.ts b/packages/signals/signals-runtime/src/web/web-signals-runtime.ts new file mode 100644 index 000000000..c80500242 --- /dev/null +++ b/packages/signals/signals-runtime/src/web/web-signals-runtime.ts @@ -0,0 +1,4 @@ +import { SignalsRuntime } from '../shared/signals-runtime' +import { Signal } from './web-signals-types' + +export class WebSignalsRuntime extends SignalsRuntime {} diff --git a/packages/signals/signals-runtime/src/web/web-signals-types.ts b/packages/signals/signals-runtime/src/web/web-signals-types.ts new file mode 100644 index 000000000..3649aea41 --- /dev/null +++ b/packages/signals/signals-runtime/src/web/web-signals-types.ts @@ -0,0 +1,101 @@ +import { BaseSignal, JSONValue } from '../shared/shared-types' + +export type SignalTypes = Signal['type'] + +export interface RawSignal extends BaseSignal { + type: T + data: Data + metadata?: Record +} + +export type InteractionData = ClickData | SubmitData | ChangeData + +interface SerializedTarget { + [key: string]: any +} + +type ClickData = { + eventType: 'click' + target: SerializedTarget +} + +type SubmitData = { + eventType: 'submit' + submitter: SerializedTarget +} + +type ChangeData = { + eventType: 'change' + [key: string]: unknown +} + +export type InteractionSignal = RawSignal<'interaction', InteractionData> + +interface BaseNavigationData { + action: ActionType + url: string + hash: string +} + +export interface URLChangeNavigationData + extends BaseNavigationData<'urlChange'> { + prevUrl: string +} + +export interface PageChangeNavigationData + extends BaseNavigationData<'pageLoad'> {} + +export type NavigationData = URLChangeNavigationData | PageChangeNavigationData + +export type NavigationSignal = RawSignal<'navigation', NavigationData> + +interface InstrumentationData { + rawEvent: unknown +} +export type InstrumentationSignal = RawSignal< + 'instrumentation', + InstrumentationData +> + +export interface NetworkSignalMetadata { + filters: { + allowed: string[] + disallowed: string[] + } +} + +interface BaseNetworkData { + action: string + url: string + data: JSONValue +} + +interface NetworkRequestData extends BaseNetworkData { + action: 'request' + url: string + method: string +} + +interface NetworkResponseData extends BaseNetworkData { + action: 'response' + url: string + status: number + ok: boolean +} + +export type NetworkData = NetworkRequestData | NetworkResponseData + +export type NetworkSignal = RawSignal<'network', NetworkData> + +export interface UserDefinedSignalData { + [key: string]: any +} + +export type UserDefinedSignal = RawSignal<'userDefined', UserDefinedSignalData> + +export type Signal = + | InteractionSignal + | NavigationSignal + | InstrumentationSignal + | NetworkSignal + | UserDefinedSignal diff --git a/packages/signals/signals-runtime/tsconfig.build.json b/packages/signals/signals-runtime/tsconfig.build.json new file mode 100644 index 000000000..c11c2b9dd --- /dev/null +++ b/packages/signals/signals-runtime/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": ["**/__tests__/**", "**/test-helpers/**"], + "compilerOptions": { + "outDir": "./dist/esm", + "declarationDir": "./dist/types" + } +} diff --git a/packages/signals/signals-runtime/tsconfig.json b/packages/signals/signals-runtime/tsconfig.json new file mode 100644 index 000000000..5dc03eda1 --- /dev/null +++ b/packages/signals/signals-runtime/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "exclude": ["node_modules", "dist", "src/generated"], + "compilerOptions": { + "module": "ESNext", // es6 modules + "target": "ES2020", // don't down-compile *too much* -- if users are using webpack, they can always transpile this library themselves + "lib": ["ES2020", "DOM", "DOM.Iterable"], // assume that consumers will be polyfilling at least down to es2020 + "moduleResolution": "node", + + "isolatedModules": true // ensure we are friendly to build systems + } +} diff --git a/packages/signals/signals/package.json b/packages/signals/signals/package.json index 2eab8be6a..38697a1c7 100644 --- a/packages/signals/signals/package.json +++ b/packages/signals/signals/package.json @@ -39,12 +39,13 @@ "watch:test": "yarn test --watch", "tsc": "yarn run -T tsc", "eslint": "yarn run -T eslint", - "concurrently": "yarn run -T concurrently --raw", + "concurrently": "yarn run -T concurrently", "jest": "yarn run -T jest", "webpack": "yarn run -T webpack" }, "dependencies": { "@segment/analytics-generic-utils": "1.2.0", + "@segment/analytics-signals-runtime": "0.0.0", "idb": "^8.0.0", "tslib": "^2.4.1" }, diff --git a/packages/signals/signals/src/core/analytics-service/index.ts b/packages/signals/signals/src/core/analytics-service/index.ts index 9232e2ae4..8efdce74d 100644 --- a/packages/signals/signals/src/core/analytics-service/index.ts +++ b/packages/signals/signals/src/core/analytics-service/index.ts @@ -1,6 +1,7 @@ import { CDNSettings } from '@segment/analytics-next' -import { AnyAnalytics, createInstrumentationSignal } from '../../types' +import { AnyAnalytics } from '../../types' import { SignalGenerator } from '../signal-generators/types' +import { createInstrumentationSignal } from '../../types/factories' type EdgeFunctionSettings = { downloadURL: string; version?: number } diff --git a/packages/signals/signals/src/core/buffer/__tests__/buffer.test.ts b/packages/signals/signals/src/core/buffer/__tests__/buffer.test.ts index c78cc743e..73970602d 100644 --- a/packages/signals/signals/src/core/buffer/__tests__/buffer.test.ts +++ b/packages/signals/signals/src/core/buffer/__tests__/buffer.test.ts @@ -1,4 +1,4 @@ -import { createInteractionSignal } from '../../../types' +import { createInteractionSignal } from '../../../types/factories' import { getSignalBuffer, SignalBuffer } from '../index' describe(getSignalBuffer, () => { diff --git a/packages/signals/signals/src/core/buffer/index.ts b/packages/signals/signals/src/core/buffer/index.ts index 8b53e5ee6..2362856ba 100644 --- a/packages/signals/signals/src/core/buffer/index.ts +++ b/packages/signals/signals/src/core/buffer/index.ts @@ -1,4 +1,4 @@ -import { Signal } from '../../types' +import { Signal } from '@segment/analytics-signals-runtime' import { openDB, DBSchema, IDBPDatabase } from 'idb' import { logger } from '../../lib/logger' diff --git a/packages/signals/signals/src/core/client/__tests__/redact.test.ts b/packages/signals/signals/src/core/client/__tests__/redact.test.ts index c205a5031..fb5d4982d 100644 --- a/packages/signals/signals/src/core/client/__tests__/redact.test.ts +++ b/packages/signals/signals/src/core/client/__tests__/redact.test.ts @@ -1,9 +1,5 @@ -import { - createInstrumentationSignal, - createInteractionSignal, - createNetworkSignal, - NetworkSignalMetadata, -} from '../../../types' +import { NetworkSignalMetadata } from '@segment/analytics-signals-runtime' +import * as factories from '../../../types/factories' import { redactJsonValues, redactSignalData } from '../redact' describe(redactJsonValues, () => { @@ -69,7 +65,7 @@ describe(redactSignalData, () => { }, } it('should return the signal as is if the type is "instrumentation"', () => { - const signal = createInstrumentationSignal({ + const signal = factories.createInstrumentationSignal({ foo: 123, } as any) expect(redactSignalData(signal)).toEqual(signal) @@ -81,11 +77,11 @@ describe(redactSignalData, () => { }) it('should redact the value in the "target" property if the type is "interaction"', () => { - const signal = createInteractionSignal({ + const signal = factories.createInteractionSignal({ eventType: 'change', target: { value: 'secret' }, }) - const expected = createInteractionSignal({ + const expected = factories.createInteractionSignal({ eventType: 'change', target: { value: 'XXX' }, }) @@ -93,7 +89,7 @@ describe(redactSignalData, () => { }) it('should redact the values in the "data" property if the type is "network"', () => { - const signal = createNetworkSignal( + const signal = factories.createNetworkSignal( { action: 'request', method: 'post', @@ -102,7 +98,7 @@ describe(redactSignalData, () => { }, metadataFixture ) - const expected = createNetworkSignal( + const expected = factories.createNetworkSignal( { action: 'request', method: 'post', @@ -115,7 +111,7 @@ describe(redactSignalData, () => { }) it('should not mutate the original signal object', () => { - const originalSignal = createInteractionSignal({ + const originalSignal = factories.createInteractionSignal({ eventType: 'click', target: { value: 'sensitiveData' }, }) diff --git a/packages/signals/signals/src/core/client/index.ts b/packages/signals/signals/src/core/client/index.ts index 74f5b6752..c352abcc8 100644 --- a/packages/signals/signals/src/core/client/index.ts +++ b/packages/signals/signals/src/core/client/index.ts @@ -1,6 +1,6 @@ import { Analytics, segmentio } from '@segment/analytics-next' import { logger } from '../../lib/logger' -import { Signal } from '../../types' +import { Signal } from '@segment/analytics-signals-runtime' import { redactSignalData } from './redact' export class SignalsIngestSettings { diff --git a/packages/signals/signals/src/core/client/redact.ts b/packages/signals/signals/src/core/client/redact.ts index dd861ee62..97a6de013 100644 --- a/packages/signals/signals/src/core/client/redact.ts +++ b/packages/signals/signals/src/core/client/redact.ts @@ -1,4 +1,4 @@ -import { Signal } from '../../types' +import { Signal } from '@segment/analytics-signals-runtime' export const redactSignalData = (signalArg: Signal): Signal => { const signal = structuredClone(signalArg) diff --git a/packages/signals/signals/src/core/emitter/index.ts b/packages/signals/signals/src/core/emitter/index.ts index 319fe0d48..47959bdcc 100644 --- a/packages/signals/signals/src/core/emitter/index.ts +++ b/packages/signals/signals/src/core/emitter/index.ts @@ -1,6 +1,6 @@ import { Emitter } from '@segment/analytics-generic-utils' import { logger } from '../../lib/logger' -import { Signal } from '../../types' +import { Signal } from '@segment/analytics-signals-runtime' export interface EmitSignal { emit: (signal: Signal) => void diff --git a/packages/signals/signals/src/core/processor/__tests__/signals-runtime.test.ts b/packages/signals/signals/src/core/processor/__tests__/signals-runtime.test.ts deleted file mode 100644 index dda0ae77a..000000000 --- a/packages/signals/signals/src/core/processor/__tests__/signals-runtime.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - InstrumentationSignal, - InteractionSignal, - createInstrumentationSignal, - createInteractionSignal, -} from '../../../types' -import { createSignalsRuntime, SignalsRuntime } from '../signals-runtime' - -describe('SignalsRuntime', () => { - let signalsRuntime: SignalsRuntime - let signal1: InstrumentationSignal - let signal2: InteractionSignal - let signal3: InteractionSignal - - beforeEach(() => { - signal1 = createInstrumentationSignal({ type: 'track' }) - signal2 = createInteractionSignal({ eventType: 'submit', submitter: {} }) - signal3 = createInteractionSignal({ eventType: 'change', target: {} }) - signalsRuntime = createSignalsRuntime([signal1, signal2, signal3]) - }) - - describe('find', () => { - it('should find following signal based on the provided function', () => { - const result = signalsRuntime.find(signal2, 'interaction', () => true) - expect(result).toEqual(signal3) - }) - - it('should return undefined if there are no following signals that match the query', () => { - const result = signalsRuntime.find(signal1, 'instrumentation', () => true) - expect(result).toEqual(undefined) - }) - - it('should filter based on predicate', () => { - const result = signalsRuntime.find( - signal1, - 'interaction', - (signal) => signal.data.eventType === 'change' - ) - expect(result).toEqual(signal3) - }) - - it('should return the first match if predicate is not provided', () => { - const result = signalsRuntime.find(signal1, 'interaction') - expect(result).toEqual(signal2) - }) - }) - - describe('filter', () => { - it('should filter signals based on the provided function', () => { - const result = signalsRuntime.filter( - (signal) => signal.type === 'interaction' - ) - expect(result).toEqual([signal2, signal3]) - }) - }) -}) diff --git a/packages/signals/signals/src/core/processor/polyfills.ts b/packages/signals/signals/src/core/processor/polyfills.ts new file mode 100644 index 000000000..4255c0a44 --- /dev/null +++ b/packages/signals/signals/src/core/processor/polyfills.ts @@ -0,0 +1,16 @@ +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/processor.ts b/packages/signals/signals/src/core/processor/processor.ts index 63a47f4a3..0a2e6da9b 100644 --- a/packages/signals/signals/src/core/processor/processor.ts +++ b/packages/signals/signals/src/core/processor/processor.ts @@ -1,5 +1,6 @@ import { logger } from '../../lib/logger' -import { AnyAnalytics, Signal } from '../../types' +import { Signal } from '@segment/analytics-signals-runtime' +import { AnyAnalytics } from '../../types' import { MethodName, Sandbox } from './sandbox' export class SignalEventProcessor { diff --git a/packages/signals/signals/src/core/processor/sandbox.ts b/packages/signals/signals/src/core/processor/sandbox.ts index ee985fa93..d183a04c6 100644 --- a/packages/signals/signals/src/core/processor/sandbox.ts +++ b/packages/signals/signals/src/core/processor/sandbox.ts @@ -1,9 +1,11 @@ import { logger } from '../../lib/logger' import { createWorkerBox, WorkerBoxAPI } from '../../lib/workerbox' import { resolvers } from './arg-resolvers' -import { AnalyticsRuntimePublicApi, Signal, AnalyticsEnums } from '../../types' -import { createSignalsRuntime } from './signals-runtime' +import { AnalyticsRuntimePublicApi } from '../../types' import { replaceBaseUrl } from '../../lib/replace-base-url' +import { Signal } from '@segment/analytics-signals-runtime' +import { getRuntimeCode } from '@segment/analytics-signals-runtime' +import { polyfills } from './polyfills' export type MethodName = | 'page' @@ -207,13 +209,13 @@ export class Sandbox { const analytics = new AnalyticsRuntime() const scope = { analytics, - ...AnalyticsEnums, } logger.debug('processing signal', { signal, scope, signals }) const code = [ + polyfills, await this.settings.processSignal, - `const createSignalsRuntime = ${createSignalsRuntime.toString()}`, - `const signals = createSignalsRuntime(${JSON.stringify(signals)})`, + getRuntimeCode(), + `signals.signalBuffer = ${JSON.stringify(signals)};`, 'try { processSignal(' + JSON.stringify(signal) + ', { analytics, signals, SignalType, EventType, NavigationAction }); } catch(err) { console.error("Process signal failed.", err); }', diff --git a/packages/signals/signals/src/core/processor/signals-runtime.ts b/packages/signals/signals/src/core/processor/signals-runtime.ts deleted file mode 100644 index 1e6ed87b0..000000000 --- a/packages/signals/signals/src/core/processor/signals-runtime.ts +++ /dev/null @@ -1,42 +0,0 @@ -// could be the buffered signals object? - -import { Signal, SignalType, SignalOfType } from '../../types' - -export type SignalsRuntime = ReturnType - -// This needs to use the function keyword so that it can be stringified and run in a sandbox -/** - * @param signals - List of signals, with the most recent signals first (LIFO). - */ -export function createSignalsRuntime(signals: Signal[]) { - /** - * @param fromSignal - signal to start searching from - * @param signalType - type of signal to find (e.g. 'interaction') - * @param predicate - optional predicate function to filter the signals (search domain only includes signals after the signal defined in `fromSignal`) - * @returns signals after the current one. - */ - function find( - fromSignal: Signal, - signalType: T, - predicate?: (signal: SignalOfType) => boolean - ): SignalOfType | undefined { - const _isSignalOfType = (signal: Signal): signal is SignalOfType => - signal.type === signalType - return signals - .slice(signals.indexOf(fromSignal) + 1) - .filter(_isSignalOfType) - .find((signal) => (predicate ? predicate(signal) : () => true)) - } - - /** - * Filter signals - includes the current signal - */ - function filter(...args: Parameters['filter']>): Signal[] { - return signals.filter(...args) - } - - return { - find, - filter, - } -} diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen.ts b/packages/signals/signals/src/core/signal-generators/dom-gen.ts index 06c4ab4d4..829478972 100644 --- a/packages/signals/signals/src/core/signal-generators/dom-gen.ts +++ b/packages/signals/signals/src/core/signal-generators/dom-gen.ts @@ -1,6 +1,9 @@ import { URLChangeObservable } from '../../lib/detect-url-change' import { logger } from '../../lib/logger' -import { createInteractionSignal, createNavigationSignal } from '../../types' +import { + createInteractionSignal, + createNavigationSignal, +} from '../../types/factories' import { SignalEmitter } from '../emitter' import { SignalGenerator } from './types' diff --git a/packages/signals/signals/src/core/signal-generators/network-gen/index.ts b/packages/signals/signals/src/core/signal-generators/network-gen/index.ts index 0baaef021..c0d09ce5d 100644 --- a/packages/signals/signals/src/core/signal-generators/network-gen/index.ts +++ b/packages/signals/signals/src/core/signal-generators/network-gen/index.ts @@ -3,7 +3,7 @@ import { NetworkSignalsFilter, NetworkSignalsFilterList, } from './network-signals-filter' -import { createNetworkSignal } from '../../../types' +import { createNetworkSignal } from '../../../types/factories' import { SignalEmitter } from '../../emitter' import { SignalsSettingsConfig } from '../../signals' import { SignalGenerator } from '../types' diff --git a/packages/signals/signals/src/core/signals/signals.ts b/packages/signals/signals/src/core/signals/signals.ts index 39438a274..207ace283 100644 --- a/packages/signals/signals/src/core/signals/signals.ts +++ b/packages/signals/signals/src/core/signals/signals.ts @@ -7,7 +7,8 @@ import { SignalGenerator, SignalGeneratorClass, } from '../signal-generators/types' -import { AnyAnalytics, Signal } from '../../types' +import { Signal } from '@segment/analytics-signals-runtime' +import { AnyAnalytics } from '../../types' import { registerGenerator } from '../signal-generators/register' import { AnalyticsService } from '../analytics-service' import { SignalEventProcessor } from '../processor/processor' diff --git a/packages/signals/signals/src/index.ts b/packages/signals/signals/src/index.ts index 425a1fed8..bcd36f1d4 100644 --- a/packages/signals/signals/src/index.ts +++ b/packages/signals/signals/src/index.ts @@ -4,10 +4,9 @@ */ export { SignalsPlugin } from './plugin/signals-plugin' export { Signals } from './core/signals' - +export type { Signal } from '@segment/analytics-signals-runtime' export type { ProcessSignal, AnalyticsRuntimePublicApi, SignalsPluginSettingsConfig, - Signal, } from './types' diff --git a/packages/signals/signals/src/index.umd.ts b/packages/signals/signals/src/index.umd.ts index f8268483b..388d009a9 100644 --- a/packages/signals/signals/src/index.umd.ts +++ b/packages/signals/signals/src/index.umd.ts @@ -3,7 +3,3 @@ */ import { SignalsPlugin } from './index' export { SignalsPlugin } // in case someone wants to use the umd module directly - -if (typeof window !== 'undefined') { - ;(window as any).SignalsPlugin = SignalsPlugin -} diff --git a/packages/signals/signals/src/plugin/signals-plugin.ts b/packages/signals/signals/src/plugin/signals-plugin.ts index 9ea239843..4992bd32a 100644 --- a/packages/signals/signals/src/plugin/signals-plugin.ts +++ b/packages/signals/signals/src/plugin/signals-plugin.ts @@ -1,7 +1,8 @@ import type { Plugin } from '@segment/analytics-next' import { Signals } from '../core/signals' import { logger } from '../lib/logger' -import { AnyAnalytics, Signal, SignalsPluginSettingsConfig } from '../types' +import { AnyAnalytics, SignalsPluginSettingsConfig } from '../types' +import { Signal } from '@segment/analytics-signals-runtime' import { assertBrowserEnv } from '../lib/assert-browser-env' import { version } from '../generated/version' diff --git a/packages/signals/signals/src/types/__tests__/create-network-signal.test.ts b/packages/signals/signals/src/types/__tests__/create-network-signal.test.ts index 1c9656112..943760653 100644 --- a/packages/signals/signals/src/types/__tests__/create-network-signal.test.ts +++ b/packages/signals/signals/src/types/__tests__/create-network-signal.test.ts @@ -1,9 +1,9 @@ import { - createNetworkSignal, NetworkData, NetworkSignalMetadata, -} from '../signals' +} from '@segment/analytics-signals-runtime' import { normalizeUrl } from '../../lib/normalize-url' +import { createNetworkSignal } from '../factories' jest.mock('../../lib/normalize-url', () => ({ normalizeUrl: jest.fn((url) => url), diff --git a/packages/signals/signals/src/types/factories.ts b/packages/signals/signals/src/types/factories.ts new file mode 100644 index 000000000..407b1ad87 --- /dev/null +++ b/packages/signals/signals/src/types/factories.ts @@ -0,0 +1,74 @@ +// types/factories.ts + +import { + InstrumentationSignal, + InteractionData, + InteractionSignal, + NavigationData, + NavigationSignal, + UserDefinedSignalData, + UserDefinedSignal, + NetworkData, + NetworkSignalMetadata, + NetworkSignal, + SegmentEvent, +} from '@segment/analytics-signals-runtime' +import { normalizeUrl } from '../lib/normalize-url' + +/** + * Factories + */ +export const createInstrumentationSignal = ( + rawEvent: SegmentEvent +): InstrumentationSignal => { + return { + type: 'instrumentation', + data: { + rawEvent, + }, + } +} + +export const createInteractionSignal = ( + data: InteractionData +): InteractionSignal => { + return { + type: 'interaction', + data, + } +} + +export const createNavigationSignal = ( + data: NavigationData +): NavigationSignal => { + return { + type: 'navigation', + data, + } +} + +export const createUserDefinedSignal = ( + data: UserDefinedSignalData +): UserDefinedSignal => { + return { + type: 'userDefined', + data, + } +} + +export const createNetworkSignal = ( + data: NetworkData, + metadata: NetworkSignalMetadata +): NetworkSignal => { + return { + type: 'network', + data: { + ...data, + url: normalizeUrl(data.url), + ...(data.action === 'request' + ? { method: data.method.toUpperCase() } + : {}), + }, + metadata: metadata, + } +} diff --git a/packages/signals/signals/src/types/index.ts b/packages/signals/signals/src/types/index.ts index e1b645494..dd8d174be 100644 --- a/packages/signals/signals/src/types/index.ts +++ b/packages/signals/signals/src/types/index.ts @@ -1,4 +1,3 @@ export * from './analytics-api' -export * from './signals' export * from './process-signal' export * from './settings' diff --git a/packages/signals/signals/src/types/json.ts b/packages/signals/signals/src/types/json.ts deleted file mode 100644 index c921ba18a..000000000 --- a/packages/signals/signals/src/types/json.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type JSONPrimitive = string | number | boolean | null -export type JSONValue = JSONPrimitive | JSONObject | JSONArray -export type JSONObject = { [member: string]: JSONValue } -export type JSONArray = JSONValue[] diff --git a/packages/signals/signals/src/types/process-signal.ts b/packages/signals/signals/src/types/process-signal.ts index f7f043b5b..906e17e7b 100644 --- a/packages/signals/signals/src/types/process-signal.ts +++ b/packages/signals/signals/src/types/process-signal.ts @@ -1,5 +1,8 @@ -import { SignalsRuntime } from '../core/processor/signals-runtime' -import { Signal } from './signals' +import { + Signal, + WebRuntimeConstants, + SignalsRuntime, +} from '@segment/analytics-signals-runtime' /** * Types for the signals runtime @@ -16,31 +19,8 @@ export interface AnalyticsRuntimePublicApi { export type ProcessSignalScope = { analytics: AnalyticsRuntimePublicApi signals: SignalsRuntime -} & typeof AnalyticsEnums +} & typeof WebRuntimeConstants export interface ProcessSignal { (signal: Signal, ctx: ProcessSignalScope): void } - -export const AnalyticsEnums = { - SignalType: Object.freeze({ - Interaction: 'interaction', - Navigation: 'navigation', - Network: 'network', - LocalData: 'localData', - Instrumentation: 'instrumentation', - UserDefined: 'userDefined', - }), - EventType: Object.freeze({ - Track: 'track', - Page: 'page', - Screen: 'screen', - Identify: 'identify', - Group: 'group', - Alias: 'alias', - }), - NavigationAction: Object.freeze({ - URLChange: 'urlChange', - PageLoad: 'pageLoad', - }), -} diff --git a/packages/signals/signals/src/types/signals.ts b/packages/signals/signals/src/types/signals.ts deleted file mode 100644 index 3bbb797f6..000000000 --- a/packages/signals/signals/src/types/signals.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { JSONValue } from '@segment/analytics-next' -import { normalizeUrl } from '../lib/normalize-url' - -export type SignalType = - | 'navigation' - | 'interaction' - | 'instrumentation' - | 'network' - | 'userDefined' - -export interface AppSignal { - type: T - data: Data - metadata?: Record -} - -export type InteractionData = ClickData | SubmitData | ChangeData - -interface SerializedTarget { - // nodeName: Node['nodeName'] - // textContent: Node['textContent'] - // nodeType: Node['nodeType'] - [key: string]: any -} - -type ClickData = { - eventType: 'click' - target: SerializedTarget -} - -type SubmitData = { - eventType: 'submit' - submitter: SerializedTarget -} - -type ChangeData = { - eventType: 'change' - target: SerializedTarget -} - -export type InteractionSignal = AppSignal<'interaction', InteractionData> - -interface BaseNavigationData { - action: ActionType - url: string - hash: string -} - -export interface URLChangeNavigationData - extends BaseNavigationData<'urlChange'> { - prevUrl: string -} - -export interface PageChangeNavigationData - extends BaseNavigationData<'pageLoad'> {} - -export type NavigationData = URLChangeNavigationData | PageChangeNavigationData - -export type NavigationSignal = AppSignal<'navigation', NavigationData> - -interface InstrumentationData { - rawEvent: unknown -} -export type InstrumentationSignal = AppSignal< - 'instrumentation', - InstrumentationData -> - -export interface NetworkSignalMetadata { - filters: { - allowed: string[] - disallowed: string[] - } -} - -interface BaseNetworkData { - action: string - url: string - data: JSONValue -} - -interface NetworkRequestData extends BaseNetworkData { - action: 'request' - url: string - method: string -} - -interface NetworkResponseData extends BaseNetworkData { - action: 'response' - url: string - status: number - ok: boolean -} - -export type NetworkData = NetworkRequestData | NetworkResponseData - -export type NetworkSignal = AppSignal<'network', NetworkData> - -export interface UserDefinedSignalData { - [key: string]: any -} - -export type UserDefinedSignal = AppSignal<'userDefined', UserDefinedSignalData> - -export type SignalOfType = T extends 'interaction' - ? InteractionSignal - : T extends 'navigation' - ? NavigationSignal - : T extends 'instrumentation' - ? InstrumentationSignal - : T extends 'userDefined' - ? UserDefinedSignal - : T extends 'network' - ? NetworkSignal - : never -/** - * Internal signal type - */ -export type Signal = - | InteractionSignal - | NavigationSignal - | InstrumentationSignal - | NetworkSignal - | UserDefinedSignal - -interface SegmentEvent { - type: string // e.g 'track' - [key: string]: any -} -/** - * Factories - */ -export const createInstrumentationSignal = ( - rawEvent: SegmentEvent -): InstrumentationSignal => { - return { - type: 'instrumentation', - data: { - rawEvent: rawEvent, - }, - } -} - -export const createInteractionSignal = ( - data: InteractionData -): InteractionSignal => { - return { - type: 'interaction', - data, - } -} - -export const createNavigationSignal = ( - data: NavigationData -): NavigationSignal => { - return { - type: 'navigation', - data, - } -} - -export const createUserDefinedSignal = ( - data: UserDefinedSignalData -): UserDefinedSignal => { - return { - type: 'userDefined', - data, - } -} - -export const createNetworkSignal = ( - data: NetworkData, - metadata: NetworkSignalMetadata -): NetworkSignal => { - return { - type: 'network', - data: { - ...data, - url: normalizeUrl(data.url), - ...(data.action === 'request' - ? { method: data.method.toUpperCase() } - : {}), - }, - metadata: metadata, - } -} diff --git a/turbo.json b/turbo.json index 35259dee4..04f823410 100644 --- a/turbo.json +++ b/turbo.json @@ -9,17 +9,32 @@ "**/webpack.config*", "**/*.ts", "**/*.tsx", + "!**/*.generated.*/*", "!**/__tests__/**", "!src/**/test-helpers/**" ], "outputs": ["**/dist/**", ".next/**"] }, + "build:esm": { + "dependsOn": ["^build:esm"], + "inputs": [ + "**/tsconfig*.json", + "**/*.ts", + "**/*.tsx", + "!**/__tests__/**", + "!src/**/test-helpers/**" + ], + "outputs": ["**/dist/esm/**"] + }, "test": { "dependsOn": ["build"] }, "test:int": { "dependsOn": ["build"] }, + "tsc": { + "cache": false + }, "watch": { "cache": false, "outputs": ["dist/**"] diff --git a/yarn.lock b/yarn.lock index c6975ff3c..ee1fc0ef5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3570,6 +3570,59 @@ __metadata: languageName: node linkType: hard +"@microsoft/api-extractor-model@npm:7.29.8": + version: 7.29.8 + resolution: "@microsoft/api-extractor-model@npm:7.29.8" + dependencies: + "@microsoft/tsdoc": ~0.15.0 + "@microsoft/tsdoc-config": ~0.17.0 + "@rushstack/node-core-library": 5.9.0 + checksum: 95a6b5df089d8bf44555f4565a6f0eda9323917266b2f4730b606aeb2c7f36df7c2cbcae9ca48a9198af7a33442cda8ce2c791e0f4c7c92f3bdaee6c3190b1f5 + languageName: node + linkType: hard + +"@microsoft/api-extractor@npm:^7.47.9": + version: 7.47.9 + resolution: "@microsoft/api-extractor@npm:7.47.9" + dependencies: + "@microsoft/api-extractor-model": 7.29.8 + "@microsoft/tsdoc": ~0.15.0 + "@microsoft/tsdoc-config": ~0.17.0 + "@rushstack/node-core-library": 5.9.0 + "@rushstack/rig-package": 0.5.3 + "@rushstack/terminal": 0.14.2 + "@rushstack/ts-command-line": 4.22.8 + lodash: ~4.17.15 + minimatch: ~3.0.3 + resolve: ~1.22.1 + semver: ~7.5.4 + source-map: ~0.6.1 + typescript: 5.4.2 + bin: + api-extractor: bin/api-extractor + checksum: 5e96654b388359bd9a1fb85ea7698921ca0f9dfa9c57e48fc9d28625107fea54863d57c82f4080686f190ae0d6fcd8b7fa7c070e7b8acb359b5cd62137e2bb23 + languageName: node + linkType: hard + +"@microsoft/tsdoc-config@npm:~0.17.0": + version: 0.17.0 + resolution: "@microsoft/tsdoc-config@npm:0.17.0" + dependencies: + "@microsoft/tsdoc": 0.15.0 + ajv: ~8.12.0 + jju: ~1.4.0 + resolve: ~1.22.2 + checksum: dd2de8247d0fc29608da83edf4ab73a21370f6ce10d089853303e91b135fdb1436ccec3bd1024f235dd3180dfe5dae7342989eadd03af55cf06f0e974e5fc213 + languageName: node + linkType: hard + +"@microsoft/tsdoc@npm:0.15.0, @microsoft/tsdoc@npm:~0.15.0": + version: 0.15.0 + resolution: "@microsoft/tsdoc@npm:0.15.0" + checksum: 3f693cff07b220b68563e3f86e9f94a9c8d0791a7446f76149c7d62ae5ed5cb4578bb48b9b5f9baa3dd9a9f77be81903c74654a41e0ca4ecf78936654952a8d4 + languageName: node + linkType: hard + "@ndhoule/clone@npm:^1.0.0": version: 1.0.0 resolution: "@ndhoule/clone@npm:1.0.0" @@ -4033,6 +4086,64 @@ __metadata: languageName: node linkType: hard +"@rushstack/node-core-library@npm:5.9.0": + version: 5.9.0 + resolution: "@rushstack/node-core-library@npm:5.9.0" + dependencies: + ajv: ~8.13.0 + ajv-draft-04: ~1.0.0 + ajv-formats: ~3.0.1 + fs-extra: ~7.0.1 + import-lazy: ~4.0.0 + jju: ~1.4.0 + resolve: ~1.22.1 + semver: ~7.5.4 + peerDependencies: + "@types/node": "*" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: beb558f118a796260f7df38b48b6669a94bbdb9711715785e0c5a426bd3a38c14721c03fc05e7a33883ec25a331ef0fb9e36438bb451ace021a7248a4f1fc74b + languageName: node + linkType: hard + +"@rushstack/rig-package@npm:0.5.3": + version: 0.5.3 + resolution: "@rushstack/rig-package@npm:0.5.3" + dependencies: + resolve: ~1.22.1 + strip-json-comments: ~3.1.1 + checksum: bf3eadfc434bff273893efd22b319fe159d0e3b95729cb32ce3ad9f4ab4b6fabe3c4dd7f03ee0ddc7b480f0d989e908349eae6d6dce3500f896728a085af7aab + languageName: node + linkType: hard + +"@rushstack/terminal@npm:0.14.2": + version: 0.14.2 + resolution: "@rushstack/terminal@npm:0.14.2" + dependencies: + "@rushstack/node-core-library": 5.9.0 + supports-color: ~8.1.1 + peerDependencies: + "@types/node": "*" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 90d38e6979737dcd97fdfdcebcc378194eed32a994341846235769273b6446b702e53e51e18fc8a373e8ed989c5622216aa6804198b8c7ae0e65cd6b103b90a1 + languageName: node + linkType: hard + +"@rushstack/ts-command-line@npm:4.22.8": + version: 4.22.8 + resolution: "@rushstack/ts-command-line@npm:4.22.8" + dependencies: + "@rushstack/terminal": 0.14.2 + "@types/argparse": 1.0.38 + argparse: ~1.0.9 + string-argv: ~0.3.1 + checksum: b0108e4b567c364a7c62b30dc3e4d17130b6f8ba16a0457c56b8c898ba84316e72726a4e043ca5183da7bf5d0189aed585ab3ac8bce5991b8e80ac94d333cd6c + languageName: node + linkType: hard + "@segment/action-emitters@npm:^1.1.2": version: 1.2.2 resolution: "@segment/action-emitters@npm:1.2.2" @@ -4212,6 +4323,16 @@ __metadata: languageName: unknown linkType: soft +"@segment/analytics-signals-runtime@0.0.0, @segment/analytics-signals-runtime@workspace:packages/signals/signals-runtime": + version: 0.0.0-use.local + resolution: "@segment/analytics-signals-runtime@workspace:packages/signals/signals-runtime" + dependencies: + "@internal/test-helpers": "workspace:^" + "@microsoft/api-extractor": ^7.47.9 + tslib: ^2.4.1 + languageName: unknown + linkType: soft + "@segment/analytics-signals@workspace:^, @segment/analytics-signals@workspace:packages/signals/signals": version: 0.0.0-use.local resolution: "@segment/analytics-signals@workspace:packages/signals/signals" @@ -4219,6 +4340,7 @@ __metadata: "@internal/config-webpack": "workspace:^" "@internal/test-helpers": "workspace:^" "@segment/analytics-generic-utils": 1.2.0 + "@segment/analytics-signals-runtime": 0.0.0 fake-indexeddb: ^6.0.0 idb: ^8.0.0 node-fetch: ^2.6.7 @@ -5880,6 +6002,13 @@ __metadata: languageName: node linkType: hard +"@types/argparse@npm:1.0.38": + version: 1.0.38 + resolution: "@types/argparse@npm:1.0.38" + checksum: 26ed7e3f1e3595efdb883a852f5205f971b798e4c28b7e30a32c5298eee596e8b45834ce831f014d250b9730819ab05acff5b31229666d3af4ba465b4697d0eb + languageName: node + linkType: hard + "@types/autocannon@npm:^7": version: 7.9.0 resolution: "@types/autocannon@npm:7.9.0" @@ -7592,6 +7721,18 @@ __metadata: languageName: node linkType: hard +"ajv-draft-04@npm:~1.0.0": + version: 1.0.0 + resolution: "ajv-draft-04@npm:1.0.0" + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: 3f11fa0e7f7359bef6608657f02ab78e9cc62b1fb7bdd860db0d00351b3863a1189c1a23b72466d2d82726cab4eb20725c76f5e7c134a89865e2bfd0e6828137 + languageName: node + linkType: hard + "ajv-formats@npm:^2.1.1": version: 2.1.1 resolution: "ajv-formats@npm:2.1.1" @@ -7606,6 +7747,20 @@ __metadata: languageName: node linkType: hard +"ajv-formats@npm:~3.0.1": + version: 3.0.1 + resolution: "ajv-formats@npm:3.0.1" + dependencies: + ajv: ^8.0.0 + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: f4e1fe232d67fcafc02eafe373a7a9962351e0439dd0736647ca75c93c3da23b430b6502c255ab4315410ae330d4f3013ac9fe226c40b2524ca93a58e786d086 + languageName: node + linkType: hard + "ajv-keywords@npm:^3.5.2": version: 3.5.2 resolution: "ajv-keywords@npm:3.5.2" @@ -7650,7 +7805,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.6.3, ajv@npm:^8.9.0": +"ajv@npm:^8.0.0, ajv@npm:^8.6.3, ajv@npm:^8.9.0, ajv@npm:~8.12.0": version: 8.12.0 resolution: "ajv@npm:8.12.0" dependencies: @@ -7662,6 +7817,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:~8.13.0": + version: 8.13.0 + resolution: "ajv@npm:8.13.0" + dependencies: + fast-deep-equal: ^3.1.3 + json-schema-traverse: ^1.0.0 + require-from-string: ^2.0.2 + uri-js: ^4.4.1 + checksum: 6de82d0b2073e645ca3300561356ddda0234f39b35d2125a8700b650509b296f41c00ab69f53178bbe25ad688bd6ac3747ab44101f2f4bd245952e8fd6ccc3c1 + languageName: node + linkType: hard + "analytics-events@npm:^2.0.2": version: 2.2.5 resolution: "analytics-events@npm:2.2.5" @@ -7678,6 +7845,7 @@ __metadata: dependencies: "@changesets/changelog-github": ^0.4.5 "@changesets/cli": ^2.23.2 + "@microsoft/api-extractor": ^7.47.9 "@npmcli/promise-spawn": ^7.0.0 "@types/express": 4 "@types/jest": ^29.5.11 @@ -7965,7 +8133,7 @@ __metadata: languageName: node linkType: hard -"argparse@npm:^1.0.7": +"argparse@npm:^1.0.7, argparse@npm:~1.0.9": version: 1.0.10 resolution: "argparse@npm:1.0.10" dependencies: @@ -12767,7 +12935,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^7.0.1": +"fs-extra@npm:^7.0.1, fs-extra@npm:~7.0.1": version: 7.0.1 resolution: "fs-extra@npm:7.0.1" dependencies: @@ -14000,6 +14168,13 @@ __metadata: languageName: node linkType: hard +"import-lazy@npm:~4.0.0": + version: 4.0.0 + resolution: "import-lazy@npm:4.0.0" + checksum: 22f5e51702134aef78890156738454f620e5fe7044b204ebc057c614888a1dd6fdf2ede0fdcca44d5c173fd64f65c985f19a51775b06967ef58cc3d26898df07 + languageName: node + linkType: hard + "import-local@npm:^3.0.2": version: 3.0.2 resolution: "import-local@npm:3.0.2" @@ -15519,6 +15694,13 @@ __metadata: languageName: node linkType: hard +"jju@npm:~1.4.0": + version: 1.4.0 + resolution: "jju@npm:1.4.0" + checksum: 3790481bd2b7827dd6336e6e3dc2dcc6d425679ba7ebde7b679f61dceb4457ea0cda330972494de608571f4973c6dfb5f70fab6f3c5037dbab19ac449a60424f + languageName: node + linkType: hard + "jmespath@npm:0.15.0": version: 0.15.0 resolution: "jmespath@npm:0.15.0" @@ -16358,7 +16540,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21": +"lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:~4.17.15": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 @@ -16982,7 +17164,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:~3.0.2": +"minimatch@npm:~3.0.2, minimatch@npm:~3.0.3": version: 3.0.8 resolution: "minimatch@npm:3.0.8" dependencies: @@ -19687,7 +19869,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.7, resolve@npm:^1.22.0, resolve@npm:^1.22.4": +"resolve@npm:^1.1.7, resolve@npm:^1.22.0, resolve@npm:^1.22.4, resolve@npm:~1.22.1, resolve@npm:~1.22.2": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -19749,7 +19931,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.1.7#~builtin, resolve@patch:resolve@^1.22.0#~builtin, resolve@patch:resolve@^1.22.4#~builtin": +"resolve@patch:resolve@^1.1.7#~builtin, resolve@patch:resolve@^1.22.0#~builtin, resolve@patch:resolve@^1.22.4#~builtin, resolve@patch:resolve@~1.22.1#~builtin, resolve@patch:resolve@~1.22.2#~builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -20254,7 +20436,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.3, semver@npm:^7.5.4": +"semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:~7.5.4": version: 7.5.4 resolution: "semver@npm:7.5.4" dependencies: @@ -20964,6 +21146,13 @@ __metadata: languageName: node linkType: hard +"string-argv@npm:~0.3.1": + version: 0.3.2 + resolution: "string-argv@npm:0.3.2" + checksum: 8703ad3f3db0b2641ed2adbb15cf24d3945070d9a751f9e74a924966db9f325ac755169007233e8985a39a6a292f14d4fee20482989b89b96e473c4221508a0f + languageName: node + linkType: hard + "string-length@npm:^4.0.1": version: 4.0.1 resolution: "string-length@npm:4.0.1" @@ -21232,7 +21421,7 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:3.1.1, strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": +"strip-json-comments@npm:3.1.1, strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1, strip-json-comments@npm:~3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" checksum: 492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 @@ -21285,7 +21474,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:8.1.1, supports-color@npm:^8.0.0, supports-color@npm:^8.1.0": +"supports-color@npm:8.1.1, supports-color@npm:^8.0.0, supports-color@npm:^8.1.0, supports-color@npm:~8.1.1": version: 8.1.1 resolution: "supports-color@npm:8.1.1" dependencies: @@ -22134,6 +22323,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:5.4.2": + version: 5.4.2 + resolution: "typescript@npm:5.4.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 96d80fde25a09bcb04d399082fb27a808a9e17c2111e43849d2aafbd642d835e4f4ef0de09b0ba795ec2a700be6c4c2c3f62bf4660c05404c948727b5bbfb32a + languageName: node + linkType: hard + "typescript@npm:^4.7.0": version: 4.9.5 resolution: "typescript@npm:4.9.5" @@ -22144,6 +22343,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@5.4.2#~builtin": + version: 5.4.2 + resolution: "typescript@patch:typescript@npm%3A5.4.2#~builtin::version=5.4.2&hash=1f5320" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: c1b669146bca5529873aae60870e243fa8140c85f57ca32c42f898f586d73ce4a6b4f6bb02ae312729e214d7f5859a0c70da3e527a116fdf5ad00c9fc733ecc6 + languageName: node + linkType: hard + "typescript@patch:typescript@^4.7.0#~builtin": version: 4.9.5 resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=23ec76" @@ -22347,7 +22556,7 @@ __metadata: languageName: node linkType: hard -"uri-js@npm:^4.2.2": +"uri-js@npm:^4.2.2, uri-js@npm:^4.4.1": version: 4.4.1 resolution: "uri-js@npm:4.4.1" dependencies: