From 1de1a32e2e8a06c7e63047f37753d9b00960f59b Mon Sep 17 00:00:00 2001 From: Seth Silesky <5115498+silesky@users.noreply.github.com> Date: Mon, 14 Jul 2025 21:22:29 -0500 Subject: [PATCH 1/2] remove signals packages --- .buildkite/pipeline.yml | 22 -- .changeset/thick-cars-compete.md | 5 - jest.config.js | 1 - meta-tests/check-dts.ts | 1 - package.json | 1 - packages/signals/signals-example/.babelrc | 7 - packages/signals/signals-example/.env.example | 4 - packages/signals/signals-example/.eslintrc.js | 7 - packages/signals/signals-example/CHANGELOG.md | 248 ------------ packages/signals/signals-example/README.md | 9 - packages/signals/signals-example/package.json | 34 -- .../signals/signals-example/public/index.html | 21 -- .../signals-example/src/components/App.tsx | 30 -- .../src/components/ComplexForm.tsx | 91 ----- .../src/components/ComplexReactHookForm.tsx | 97 ----- .../signals/signals-example/src/index.tsx | 6 - .../signals-example/src/lib/analytics.ts | 64 ---- .../signals-example/src/pages/Home.tsx | 15 - .../signals-example/src/pages/Other.tsx | 19 - .../src/pages/ReactHookForm.tsx | 15 - .../signals/signals-example/src/styles.css | 46 --- .../signals/signals-example/tsconfig.json | 14 - .../signals/signals-example/webpack.config.js | 65 ---- .../signals-integration-tests/.babelrc | 7 - .../signals-integration-tests/.eslintrc.js | 7 - .../.lintstagedrc.js | 1 - .../signals-integration-tests/jest.config.js | 3 - .../signals-integration-tests/package.json | 43 --- .../playwright.config.ts | 55 --- .../playwright.global-setup.ts | 13 - .../src/helpers/base-page-object.ts | 281 -------------- .../src/helpers/env-config.ts | 10 - .../src/helpers/fixtures.ts | 15 - .../src/helpers/log-console.ts | 16 - .../src/helpers/network-utils.ts | 232 ------------ .../src/helpers/playwright-utils.ts | 51 --- .../src/helpers/ts.ts | 1 - .../signals-integration-tests/src/shims.d.ts | 10 - .../tests/custom-elements/components/App.tsx | 23 -- .../custom-elements/components/Button.css | 42 --- .../custom-elements/components/Button.tsx | 7 - .../custom-elements/components/Checkbox.css | 99 ----- .../custom-elements/components/Checkbox.tsx | 26 -- .../custom-elements/components/ComboBox.css | 125 ------- .../custom-elements/components/ComboBox.tsx | 52 --- .../custom-elements/components/Dialog.css | 17 - .../custom-elements/components/Dialog.tsx | 7 - .../tests/custom-elements/components/Form.css | 32 -- .../tests/custom-elements/components/Form.tsx | 7 - .../custom-elements/components/ListBox.css | 223 ----------- .../custom-elements/components/ListBox.tsx | 20 - .../custom-elements/components/Modal.css | 84 ----- .../custom-elements/components/Modal.tsx | 7 - .../custom-elements/components/Popover.css | 87 ----- .../custom-elements/components/Popover.tsx | 26 -- .../custom-elements/components/Select.css | 139 ------- .../custom-elements/components/Select.tsx | 58 --- .../custom-elements/components/Switch.css | 74 ---- .../custom-elements/components/Switch.tsx | 19 - .../tests/custom-elements/components/Tabs.css | 102 ----- .../tests/custom-elements/components/Tabs.tsx | 7 - .../custom-elements/components/TextField.css | 49 --- .../custom-elements/components/TextField.tsx | 35 -- .../custom-elements/components/theme.css | 127 ------- .../custom-elements/custom-select.test.ts | 37 -- .../custom-elements/custom-textfield.test.ts | 43 --- .../src/tests/custom-elements/index-page.ts | 7 - .../tests/custom-elements/index.bundle.tsx | 12 - .../src/tests/custom-elements/index.html | 14 - .../src/tests/performance/index-page.ts | 7 - .../src/tests/performance/index.html | 14 - .../src/tests/performance/memory-leak.test.ts | 97 ----- .../all-segment-events.test.ts | 99 ----- .../src/tests/signals-vanilla/basic.test.ts | 190 ---------- .../button-click-complex.test.ts | 71 ---- .../signals-vanilla/change-input.test.ts | 55 --- .../src/tests/signals-vanilla/index-page.ts | 53 --- .../src/tests/signals-vanilla/index.bundle.ts | 8 - .../src/tests/signals-vanilla/index.html | 29 -- .../tests/signals-vanilla/middleware.test.ts | 61 --- .../network-signals-allow-list.test.ts | 46 --- .../network-signals-fetch.test.ts | 278 -------------- .../network-signals-xhr.test.ts | 333 ----------------- .../src/tests/signals-vanilla/reset.test.ts | 55 --- .../signals-vanilla/signals-find.test.ts | 24 -- .../signals-vanilla/signals-ingestion.test.ts | 66 ---- .../signals-vanilla/signals-redaction.test.ts | 62 --- .../all-segment-events-snapshot.json | 220 ----------- .../top-level-metadata.test.ts | 71 ---- .../signals-integration-tests/tsconfig.json | 11 - .../webpack.config.ts | 53 --- packages/signals/signals/.eslintrc.js | 7 - packages/signals/signals/.lintstagedrc.js | 5 - packages/signals/signals/CHANGELOG.md | 287 -------------- packages/signals/signals/LICENSE | 324 ---------------- packages/signals/signals/jest.config.js | 6 - packages/signals/signals/jest.setup.ts | 6 - packages/signals/signals/package.json | 67 ---- .../signals/scripts/assert-workerbox-built.sh | 16 - .../signals/scripts/build-workerbox.js | 64 ---- packages/signals/signals/scripts/version.sh | 11 - .../__tests__/analytics-service.test.ts | 111 ------ .../src/core/analytics-service/index.ts | 56 --- .../src/core/buffer/__tests__/buffer.test.ts | 108 ------ .../signals/signals/src/core/buffer/index.ts | 226 ----------- .../signals/src/core/debug-mode/index.ts | 35 -- .../emitter/__tests__/signal-emitter.test.ts | 353 ------------------ .../signals/signals/src/core/emitter/index.ts | 204 ---------- .../core/middleware/event-processor/index.ts | 57 --- .../__tests__/network-signals-filter.test.ts | 72 ---- .../network-signals-filter.ts | 159 -------- .../signals-ingest/__tests__/client.test.ts | 95 ----- .../signals-ingest/__tests__/redact.test.ts | 162 -------- .../core/middleware/signals-ingest/index.ts | 18 - .../core/middleware/signals-ingest/redact.ts | 118 ------ .../signals-ingest/signals-ingest-client.ts | 95 ----- .../src/core/middleware/user-info/index.ts | 17 - .../__tests__/sandbox-settings.test.ts | 52 --- .../src/core/processor/arg-resolvers.ts | 15 - .../signals/src/core/processor/polyfills.ts | 16 - .../signals/src/core/processor/processor.ts | 39 -- .../signals/src/core/processor/sandbox.ts | 333 ----------------- .../dom-gen/__tests__/change-gen.test.ts | 192 ---------- .../dom-gen/__tests__/clean-text.test.ts | 51 --- .../dom-gen/__tests__/element-parser.test.ts | 279 -------------- .../__tests__/mutation-observer.test.ts | 202 ---------- .../dom-gen/__tests__/navigation-gen.test.ts | 178 --------- .../signal-generators/dom-gen/change-gen.ts | 201 ---------- .../core/signal-generators/dom-gen/dom-gen.ts | 76 ---- .../dom-gen/element-parser.ts | 180 --------- .../core/signal-generators/dom-gen/helpers.ts | 17 - .../core/signal-generators/dom-gen/index.ts | 17 - .../dom-gen/mutation-observer.ts | 341 ----------------- .../dom-gen/navigation-gen.ts | 65 ---- .../network-gen/__tests__/helpers.test.ts | 117 ------ .../__tests__/network-generator.test.ts | 198 ---------- .../__tests__/network-interceptor.test.ts | 165 -------- .../signal-generators/network-gen/helpers.ts | 96 ----- .../signal-generators/network-gen/index.ts | 78 ---- .../network-gen/network-interceptor.ts | 313 ---------------- .../src/core/signal-generators/register.ts | 26 -- .../src/core/signal-generators/types.ts | 20 - .../signals/signals/src/core/signals/index.ts | 3 - .../signals/src/core/signals/settings.ts | 183 --------- .../signals/src/core/signals/signals.ts | 141 ------- .../signals/signals/src/generated/version.ts | 2 - packages/signals/signals/src/index.ts | 16 - packages/signals/signals/src/index.umd.ts | 11 - .../src/lib/assert-browser-env/index.ts | 5 - .../lib/debounce/__tests__/debounce.test.ts | 48 --- .../signals/signals/src/lib/debounce/index.ts | 42 --- .../src/lib/detect-url-change/index.ts | 46 --- .../signals/signals/src/lib/exists/index.ts | 7 - .../signals/src/lib/load-script/index.ts | 66 ---- .../signals/signals/src/lib/logger/index.ts | 65 ---- .../__tests__/normalize-url.test.ts | 51 --- .../signals/src/lib/normalize-url/index.ts | 15 - .../signals/src/lib/page-data/index.ts | 13 - .../__tests__/replace-base-url.test.ts | 38 -- .../signals/src/lib/replace-base-url/index.ts | 10 - .../lib/storage/__tests__/web-storage.test.ts | 65 ---- .../signals/src/lib/storage/web-storage.ts | 37 -- .../src/lib/workerbox/__mocks__/workerbox.ts | 13 - .../signals/src/lib/workerbox/index.ts | 251 ------------- .../src/lib/workerbox/worker.generated.ts | 3 - .../signals/src/lib/workerbox/worker.html | 13 - .../signals/src/lib/workerbox/worker.ts | 115 ------ .../plugin/__tests__/signals-plugin.test.ts | 53 --- .../signals/src/plugin/signals-plugin.ts | 83 ---- .../signals/src/test-helpers/jest-extended.ts | 47 --- .../src/test-helpers/mocks/analytics-mock.ts | 33 -- .../src/test-helpers/mocks/factories.ts | 13 - .../signals/src/test-helpers/mocks/index.ts | 1 - .../signals/signals/src/test-helpers/range.ts | 1 - .../signals/src/test-helpers/set-location.ts | 9 - .../__tests__/create-network-signal.test.ts | 109 ------ .../signals/src/types/analytics-api.ts | 107 ------ .../signals/signals/src/types/factories.ts | 85 ----- packages/signals/signals/src/types/index.ts | 3 - .../signals/src/types/process-signal.ts | 23 -- .../signals/signals/src/types/settings.ts | 163 -------- packages/signals/signals/src/utils/index.ts | 1 - .../signals/signals/src/utils/is-class.ts | 5 - .../signals/signals/src/utils/ts-helpers.ts | 35 -- packages/signals/signals/tsconfig.build.json | 10 - packages/signals/signals/tsconfig.json | 11 - packages/signals/signals/webpack.config.js | 31 -- .../standalone-playground/package.json | 4 +- .../pages/index-signals.html | 126 ------- scripts/package.json | 1 - 190 files changed, 1 insertion(+), 13235 deletions(-) delete mode 100644 .changeset/thick-cars-compete.md delete mode 100644 packages/signals/signals-example/.babelrc delete mode 100644 packages/signals/signals-example/.env.example delete mode 100644 packages/signals/signals-example/.eslintrc.js delete mode 100644 packages/signals/signals-example/CHANGELOG.md delete mode 100644 packages/signals/signals-example/README.md delete mode 100644 packages/signals/signals-example/package.json delete mode 100644 packages/signals/signals-example/public/index.html delete mode 100644 packages/signals/signals-example/src/components/App.tsx delete mode 100644 packages/signals/signals-example/src/components/ComplexForm.tsx delete mode 100644 packages/signals/signals-example/src/components/ComplexReactHookForm.tsx delete mode 100644 packages/signals/signals-example/src/index.tsx delete mode 100644 packages/signals/signals-example/src/lib/analytics.ts delete mode 100644 packages/signals/signals-example/src/pages/Home.tsx delete mode 100644 packages/signals/signals-example/src/pages/Other.tsx delete mode 100644 packages/signals/signals-example/src/pages/ReactHookForm.tsx delete mode 100644 packages/signals/signals-example/src/styles.css delete mode 100644 packages/signals/signals-example/tsconfig.json delete mode 100644 packages/signals/signals-example/webpack.config.js delete mode 100644 packages/signals/signals-integration-tests/.babelrc delete mode 100644 packages/signals/signals-integration-tests/.eslintrc.js delete mode 100644 packages/signals/signals-integration-tests/.lintstagedrc.js delete mode 100644 packages/signals/signals-integration-tests/jest.config.js delete mode 100644 packages/signals/signals-integration-tests/package.json delete mode 100644 packages/signals/signals-integration-tests/playwright.config.ts delete mode 100644 packages/signals/signals-integration-tests/playwright.global-setup.ts delete mode 100644 packages/signals/signals-integration-tests/src/helpers/base-page-object.ts delete mode 100644 packages/signals/signals-integration-tests/src/helpers/env-config.ts delete mode 100644 packages/signals/signals-integration-tests/src/helpers/fixtures.ts delete mode 100644 packages/signals/signals-integration-tests/src/helpers/log-console.ts delete mode 100644 packages/signals/signals-integration-tests/src/helpers/network-utils.ts delete mode 100644 packages/signals/signals-integration-tests/src/helpers/playwright-utils.ts delete mode 100644 packages/signals/signals-integration-tests/src/helpers/ts.ts delete mode 100644 packages/signals/signals-integration-tests/src/shims.d.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/App.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.css delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.css delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.css delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.css delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.css delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.css delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.css delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.css delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.css delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.css delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.css delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.css delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/components/theme.css delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/custom-select.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/custom-textfield.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/index-page.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/index.bundle.tsx delete mode 100644 packages/signals/signals-integration-tests/src/tests/custom-elements/index.html delete mode 100644 packages/signals/signals-integration-tests/src/tests/performance/index-page.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/performance/index.html delete mode 100644 packages/signals/signals-integration-tests/src/tests/performance/memory-leak.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/button-click-complex.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/index-page.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.bundle.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.html delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/middleware.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-allow-list.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-fetch.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-xhr.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/reset.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-find.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/snapshots/all-segment-events-snapshot.json delete mode 100644 packages/signals/signals-integration-tests/src/tests/signals-vanilla/top-level-metadata.test.ts delete mode 100644 packages/signals/signals-integration-tests/tsconfig.json delete mode 100644 packages/signals/signals-integration-tests/webpack.config.ts delete mode 100644 packages/signals/signals/.eslintrc.js delete mode 100644 packages/signals/signals/.lintstagedrc.js delete mode 100644 packages/signals/signals/CHANGELOG.md delete mode 100644 packages/signals/signals/LICENSE delete mode 100644 packages/signals/signals/jest.config.js delete mode 100644 packages/signals/signals/jest.setup.ts delete mode 100644 packages/signals/signals/package.json delete mode 100644 packages/signals/signals/scripts/assert-workerbox-built.sh delete mode 100644 packages/signals/signals/scripts/build-workerbox.js delete mode 100644 packages/signals/signals/scripts/version.sh delete mode 100644 packages/signals/signals/src/core/analytics-service/__tests__/analytics-service.test.ts delete mode 100644 packages/signals/signals/src/core/analytics-service/index.ts delete mode 100644 packages/signals/signals/src/core/buffer/__tests__/buffer.test.ts delete mode 100644 packages/signals/signals/src/core/buffer/index.ts delete mode 100644 packages/signals/signals/src/core/debug-mode/index.ts delete mode 100644 packages/signals/signals/src/core/emitter/__tests__/signal-emitter.test.ts delete mode 100644 packages/signals/signals/src/core/emitter/index.ts delete mode 100644 packages/signals/signals/src/core/middleware/event-processor/index.ts delete mode 100644 packages/signals/signals/src/core/middleware/network-signals-filter/__tests__/network-signals-filter.test.ts delete mode 100644 packages/signals/signals/src/core/middleware/network-signals-filter/network-signals-filter.ts delete mode 100644 packages/signals/signals/src/core/middleware/signals-ingest/__tests__/client.test.ts delete mode 100644 packages/signals/signals/src/core/middleware/signals-ingest/__tests__/redact.test.ts delete mode 100644 packages/signals/signals/src/core/middleware/signals-ingest/index.ts delete mode 100644 packages/signals/signals/src/core/middleware/signals-ingest/redact.ts delete mode 100644 packages/signals/signals/src/core/middleware/signals-ingest/signals-ingest-client.ts delete mode 100644 packages/signals/signals/src/core/middleware/user-info/index.ts delete mode 100644 packages/signals/signals/src/core/processor/__tests__/sandbox-settings.test.ts delete mode 100644 packages/signals/signals/src/core/processor/arg-resolvers.ts delete mode 100644 packages/signals/signals/src/core/processor/polyfills.ts delete mode 100644 packages/signals/signals/src/core/processor/processor.ts delete mode 100644 packages/signals/signals/src/core/processor/sandbox.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/change-gen.test.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/clean-text.test.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/element-parser.test.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/navigation-gen.test.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/dom-gen.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/element-parser.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/helpers.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/index.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/dom-gen/navigation-gen.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/network-gen/__tests__/helpers.test.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-generator.test.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-interceptor.test.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/network-gen/helpers.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/network-gen/index.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/network-gen/network-interceptor.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/register.ts delete mode 100644 packages/signals/signals/src/core/signal-generators/types.ts delete mode 100644 packages/signals/signals/src/core/signals/index.ts delete mode 100644 packages/signals/signals/src/core/signals/settings.ts delete mode 100644 packages/signals/signals/src/core/signals/signals.ts delete mode 100644 packages/signals/signals/src/generated/version.ts delete mode 100644 packages/signals/signals/src/index.ts delete mode 100644 packages/signals/signals/src/index.umd.ts delete mode 100644 packages/signals/signals/src/lib/assert-browser-env/index.ts delete mode 100644 packages/signals/signals/src/lib/debounce/__tests__/debounce.test.ts delete mode 100644 packages/signals/signals/src/lib/debounce/index.ts delete mode 100644 packages/signals/signals/src/lib/detect-url-change/index.ts delete mode 100644 packages/signals/signals/src/lib/exists/index.ts delete mode 100644 packages/signals/signals/src/lib/load-script/index.ts delete mode 100644 packages/signals/signals/src/lib/logger/index.ts delete mode 100644 packages/signals/signals/src/lib/normalize-url/__tests__/normalize-url.test.ts delete mode 100644 packages/signals/signals/src/lib/normalize-url/index.ts delete mode 100644 packages/signals/signals/src/lib/page-data/index.ts delete mode 100644 packages/signals/signals/src/lib/replace-base-url/__tests__/replace-base-url.test.ts delete mode 100644 packages/signals/signals/src/lib/replace-base-url/index.ts delete mode 100644 packages/signals/signals/src/lib/storage/__tests__/web-storage.test.ts delete mode 100644 packages/signals/signals/src/lib/storage/web-storage.ts delete mode 100644 packages/signals/signals/src/lib/workerbox/__mocks__/workerbox.ts delete mode 100644 packages/signals/signals/src/lib/workerbox/index.ts delete mode 100644 packages/signals/signals/src/lib/workerbox/worker.generated.ts delete mode 100644 packages/signals/signals/src/lib/workerbox/worker.html delete mode 100644 packages/signals/signals/src/lib/workerbox/worker.ts delete mode 100644 packages/signals/signals/src/plugin/__tests__/signals-plugin.test.ts delete mode 100644 packages/signals/signals/src/plugin/signals-plugin.ts delete mode 100644 packages/signals/signals/src/test-helpers/jest-extended.ts delete mode 100644 packages/signals/signals/src/test-helpers/mocks/analytics-mock.ts delete mode 100644 packages/signals/signals/src/test-helpers/mocks/factories.ts delete mode 100644 packages/signals/signals/src/test-helpers/mocks/index.ts delete mode 100644 packages/signals/signals/src/test-helpers/range.ts delete mode 100644 packages/signals/signals/src/test-helpers/set-location.ts delete mode 100644 packages/signals/signals/src/types/__tests__/create-network-signal.test.ts delete mode 100644 packages/signals/signals/src/types/analytics-api.ts delete mode 100644 packages/signals/signals/src/types/factories.ts delete mode 100644 packages/signals/signals/src/types/index.ts delete mode 100644 packages/signals/signals/src/types/process-signal.ts delete mode 100644 packages/signals/signals/src/types/settings.ts delete mode 100644 packages/signals/signals/src/utils/index.ts delete mode 100644 packages/signals/signals/src/utils/is-class.ts delete mode 100644 packages/signals/signals/src/utils/ts-helpers.ts delete mode 100644 packages/signals/signals/tsconfig.build.json delete mode 100644 packages/signals/signals/tsconfig.json delete mode 100644 packages/signals/signals/webpack.config.js delete mode 100644 playgrounds/standalone-playground/pages/index-signals.html diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 72d6c76cd..21e5038f2 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -164,28 +164,6 @@ steps: - ssh://git@github.com/segmentio/cache-buildkite-plugin#v2.0.0: key: "v1.1-cache-dev-{{ checksum 'yarn.lock' }}" - - label: '[Signals] Lint + Test' - agents: - queue: v1 - commands: - - npm config set "//registry.npmjs.org/:_authToken" $${NPM_TOKEN} - - echo "--- Install dependencies" - - HUSKY=0 yarn install --immutable - - echo "--- Build bundles" - - yarn turbo run --filter='./packages/signals/*' build - - echo "+++ Assert Generated Files Up-to-Date" - - yarn turbo run --filter='./packages/signals/*' assert-generated - - echo "+++ Run Lint" - - yarn turbo run --filter='./packages/signals/*' lint - - echo "+++ Run Tests" - - yarn turbo run --filter='./packages/signals/*' test - - echo "+++ Run Integration Tests" - - yarn turbo run --filter='./packages/signals/*' test:int - plugins: - - ssh://git@github.com/segmentio/cache-buildkite-plugin#v2.0.0: - key: "v1.1-cache-dev-{{ checksum 'yarn.lock' }}" - paths: ['.yarn/cache/'] - - label: ':thisisfine: [Browser] Destinations QA / E2E' key: destinations agents: diff --git a/.changeset/thick-cars-compete.md b/.changeset/thick-cars-compete.md deleted file mode 100644 index b60d91d05..000000000 --- a/.changeset/thick-cars-compete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@segment/analytics-signals': major ---- - -Update package to use new spec diff --git a/jest.config.js b/jest.config.js index f59880664..e005eaf10 100644 --- a/jest.config.js +++ b/jest.config.js @@ -17,7 +17,6 @@ module.exports = () => '/packages/consent/consent-tools', '/packages/consent/consent-wrapper-onetrust', '/scripts', - '/packages/signals/signals', '/packages/test-helpers', ], }) diff --git a/meta-tests/check-dts.ts b/meta-tests/check-dts.ts index cdb262cee..2c9db366b 100644 --- a/meta-tests/check-dts.ts +++ b/meta-tests/check-dts.ts @@ -18,7 +18,6 @@ const publicPackageNames = [ '@segment/analytics-next', '@segment/analytics-core', '@segment/analytics-node', - '@segment/analytics-signals', ] const allPublicPackageDirNames = packages diff --git a/package.json b/package.json index ad42101ba..ad04ffbfe 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "playgrounds/*", "packages/*", "packages/consent/*", - "packages/signals/*", "scripts" ], "engines": { diff --git a/packages/signals/signals-example/.babelrc b/packages/signals/signals-example/.babelrc deleted file mode 100644 index 701fada78..000000000 --- a/packages/signals/signals-example/.babelrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "presets": [ - "@babel/preset-env", - "@babel/preset-react", - "@babel/preset-typescript" - ] -} \ No newline at end of file diff --git a/packages/signals/signals-example/.env.example b/packages/signals/signals-example/.env.example deleted file mode 100644 index d0e027782..000000000 --- a/packages/signals/signals-example/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -# Configure + rename this file to .env - -WRITEKEY=your-write-key -STAGE=false \ No newline at end of file diff --git a/packages/signals/signals-example/.eslintrc.js b/packages/signals/signals-example/.eslintrc.js deleted file mode 100644 index eedf2f3c3..000000000 --- a/packages/signals/signals-example/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type { import('eslint').Linter.Config } */ -module.exports = { - extends: ['../../../.eslintrc'], - env: { - browser: true, - }, -} diff --git a/packages/signals/signals-example/CHANGELOG.md b/packages/signals/signals-example/CHANGELOG.md deleted file mode 100644 index 5b65f5dfc..000000000 --- a/packages/signals/signals-example/CHANGELOG.md +++ /dev/null @@ -1,248 +0,0 @@ -# @internal/signals-example - -## null - -### Patch Changes - -- Updated dependencies []: - - @segment/analytics-next@1.81.1 - - @segment/analytics-signals@1.13.1 - -## null - -### Patch Changes - -- Updated dependencies [[`093ecf65`](https://github.com/segmentio/analytics-next/commit/093ecf65c9327fa7ac4753e55620819841d4f43c)]: - - @segment/analytics-signals@1.13.1 - -## null - -### Patch Changes - -- Updated dependencies [[`64835418`](https://github.com/segmentio/analytics-next/commit/64835418f9dc6cc16fbc6fa7580c565dfb3a446a), [`64835418`](https://github.com/segmentio/analytics-next/commit/64835418f9dc6cc16fbc6fa7580c565dfb3a446a)]: - - @segment/analytics-signals@1.13.0 - -## null - -### Patch Changes - -- Updated dependencies [[`37df4c96`](https://github.com/segmentio/analytics-next/commit/37df4c96c50db9fdde3e9ab1a37b72299a504a5c)]: - - @segment/analytics-next@1.81.0 - - @segment/analytics-signals@1.12.1 - -## null - -### Patch Changes - -- Updated dependencies [[`f2c2b764`](https://github.com/segmentio/analytics-next/commit/f2c2b764c168b954218f1fedce19c1aabfb5d26d), [`a00fa28d`](https://github.com/segmentio/analytics-next/commit/a00fa28d76af890f1e2adb69ea0b0860beafca15)]: - - @segment/analytics-next@1.80.0 - - @segment/analytics-signals@1.12.1 - -## null - -### Patch Changes - -- Updated dependencies [[`6753b4b4`](https://github.com/segmentio/analytics-next/commit/6753b4b40d5378734b51369510e707e813a0ad5e), [`c91efe39`](https://github.com/segmentio/analytics-next/commit/c91efe397318cd6f333681b0537e5eb60b99d013)]: - - @segment/analytics-signals@1.12.0 - - @segment/analytics-next@1.79.0 - -## null - -### Patch Changes - -- Updated dependencies [[`b538682b`](https://github.com/segmentio/analytics-next/commit/b538682bc67911fb40e84674136a6551e14e48cf)]: - - @segment/analytics-next@1.78.1 - - @segment/analytics-signals@1.11.1 - -## null - -### Patch Changes - -- Updated dependencies [[`ee838db5`](https://github.com/segmentio/analytics-next/commit/ee838db5b361fc52a400ab2fb8bae50bff4d262b)]: - - @segment/analytics-next@1.78.0 - - @segment/analytics-signals@1.11.1 - -## null - -### Patch Changes - -- Updated dependencies [[`0596bc45`](https://github.com/segmentio/analytics-next/commit/0596bc455b9ecf8ed179f1be5decb0a4b89bb9a5)]: - - @segment/analytics-signals@1.11.0 - -## null - -### Patch Changes - -- Updated dependencies [[`9b470331`](https://github.com/segmentio/analytics-next/commit/9b470331584e02dd883942ae83300c9eb971bc95), [`91dab927`](https://github.com/segmentio/analytics-next/commit/91dab9273954bc26dbcb579a387787f5a0cc185e)]: - - @segment/analytics-signals@1.10.3 - -## null - -### Patch Changes - -- Updated dependencies [[`2fc749a1`](https://github.com/segmentio/analytics-next/commit/2fc749a17b14b2667df76ecce685aefb6656eaae)]: - - @segment/analytics-signals@1.10.2 - -## null - -### Patch Changes - -- Updated dependencies [[`e0ed6a5a`](https://github.com/segmentio/analytics-next/commit/e0ed6a5a072bcb859a2ae304e572e03284d262de)]: - - @segment/analytics-signals@1.10.1 - -## null - -### Patch Changes - -- Updated dependencies [[`171080cc`](https://github.com/segmentio/analytics-next/commit/171080cc9ca198b9f89a9e9154c2a78ed8ef29ee), [`bf868573`](https://github.com/segmentio/analytics-next/commit/bf8685737466cb1193a54f99871ec7348b8616d8)]: - - @segment/analytics-signals@1.10.0 - -## null - -### Patch Changes - -- Updated dependencies [[`9a8b0e03`](https://github.com/segmentio/analytics-next/commit/9a8b0e0322a4291a3ee3c5c06974a0af9ea5469f)]: - - @segment/analytics-signals@1.9.2 - -## null - -### Patch Changes - -- Updated dependencies [[`8e0162b9`](https://github.com/segmentio/analytics-next/commit/8e0162b9553419448b7975337a53fa1c66e70d47), [`8e0162b9`](https://github.com/segmentio/analytics-next/commit/8e0162b9553419448b7975337a53fa1c66e70d47)]: - - @segment/analytics-next@1.77.0 - - @segment/analytics-signals@1.9.1 - -## null - -### Patch Changes - -- Updated dependencies [[`32582be8`](https://github.com/segmentio/analytics-next/commit/32582be828d112a31a92b16e80b15727c3a36613), [`9d1b042d`](https://github.com/segmentio/analytics-next/commit/9d1b042d0bb09996e5c8674b7b3e4dd3bf138e3d), [`9883151f`](https://github.com/segmentio/analytics-next/commit/9883151f85d555266e40e69e876f1dec8a632c5a)]: - - @segment/analytics-next@1.76.1 - - @segment/analytics-signals@1.9.0 - -## null - -### Patch Changes - -- Updated dependencies [[`00a736f3`](https://github.com/segmentio/analytics-next/commit/00a736f31326328e91c9cae0b244b9db9b0791fc)]: - - @segment/analytics-signals@1.9.0 - -## null - -### Patch Changes - -- Updated dependencies [[`3410160c`](https://github.com/segmentio/analytics-next/commit/3410160c30024c292f252802cdb98b6b59fced0c), [`46e88198`](https://github.com/segmentio/analytics-next/commit/46e88198b2f9d3a835e02fa22317d784c6f71ebf), [`de6f86dc`](https://github.com/segmentio/analytics-next/commit/de6f86dc637dbc49f5bb55c1e44a36a2011c14b9), [`342868cb`](https://github.com/segmentio/analytics-next/commit/342868cb9db7da37d8851dadca4b1b1dc0ecd923)]: - - @segment/analytics-signals@1.8.0 - -## null - -### Patch Changes - -- Updated dependencies [[`bf85047e`](https://github.com/segmentio/analytics-next/commit/bf85047e971add497d5c9ab72972394b1f27e887)]: - - @segment/analytics-signals@1.7.1 - -## null - -### Patch Changes - -- Updated dependencies [[`f50bd0f5`](https://github.com/segmentio/analytics-next/commit/f50bd0f5fc30840af33992107cb0a5da432a0b1b), [`ccc97f1b`](https://github.com/segmentio/analytics-next/commit/ccc97f1b61f90c6e07154e205d79952fc579fae1)]: - - @segment/analytics-signals@1.7.0 - -## null - -### Patch Changes - -- Updated dependencies [[`6fff4114`](https://github.com/segmentio/analytics-next/commit/6fff4114fb2cc9267362d8a3812ad96ec85a1dac)]: - - @segment/analytics-signals@1.6.0 - -## null - -### Patch Changes - -- Updated dependencies [[`ed7a749b`](https://github.com/segmentio/analytics-next/commit/ed7a749be7cddcbf656ac9f72e444ea9f822a718)]: - - @segment/analytics-signals@1.5.1 - -## null - -### Patch Changes - -- Updated dependencies [[`7d5d9753`](https://github.com/segmentio/analytics-next/commit/7d5d9753509d8af8f10486c91505b30d2c6e240a), [`11a943e2`](https://github.com/segmentio/analytics-next/commit/11a943e29e73189c613f93b268e10a64f2561fbc), [`9e6db285`](https://github.com/segmentio/analytics-next/commit/9e6db2857798f4b5bfdbbfe3570b3d4d83294a79), [`ba2f2b16`](https://github.com/segmentio/analytics-next/commit/ba2f2b165bf1b997a9ce79d410690d27d50378fd), [`08e45530`](https://github.com/segmentio/analytics-next/commit/08e4553001da146f1d80a9b620aef0ef0db04bd4)]: - - @segment/analytics-signals@1.5.0 - - @segment/analytics-next@1.76.0 - -## null - -### Patch Changes - -- Updated dependencies [[`c8ce6144`](https://github.com/segmentio/analytics-next/commit/c8ce6144b31bddfc66961e979d5648fb66e102e5), [`29d17003`](https://github.com/segmentio/analytics-next/commit/29d1700303d0384fbd01edee9e9ff231f35de9ef), [`bedea03b`](https://github.com/segmentio/analytics-next/commit/bedea03bb50e01a7df71461087a9ec340375906d)]: - - @segment/analytics-signals@1.4.0 - - @segment/analytics-next@1.75.0 - -## null - -### Patch Changes - -- Updated dependencies [[`3f58366b`](https://github.com/segmentio/analytics-next/commit/3f58366b0e01aa723d9d3bbb9fe8549d3082eb8e), [`29e856f9`](https://github.com/segmentio/analytics-next/commit/29e856f9f36088a0dc625014ebda8e09fc3b621e), [`c45d445b`](https://github.com/segmentio/analytics-next/commit/c45d445beb1b1d5b03738557720720d05e9c08a3)]: - - @segment/analytics-next@1.74.0 - - @segment/analytics-signals@1.3.0 - -## null - -### Patch Changes - -- Updated dependencies [[`101e8414`](https://github.com/segmentio/analytics-next/commit/101e841404e5f55f53ba014b6195bf1066aeb67e), [`571386f5`](https://github.com/segmentio/analytics-next/commit/571386f5d388ed3ff44520ee94795424378950ed)]: - - @segment/analytics-signals@1.2.0 - -## null - -### Patch Changes - -- Updated dependencies [[`04a7cc85`](https://github.com/segmentio/analytics-next/commit/04a7cc85247bdcdb832d0cca4ddbb4391ccada3a), [`5647624c`](https://github.com/segmentio/analytics-next/commit/5647624cbcd4984e5bdbf2e9c907619366864c4e), [`784ddf21`](https://github.com/segmentio/analytics-next/commit/784ddf21906a2a72c1ccea41d0ba323e189c4010)]: - - @segment/analytics-signals@1.1.0 - - @segment/analytics-next@1.73.0 - -## null - -### Patch Changes - -- Updated dependencies [[`549b028`](https://github.com/segmentio/analytics-next/commit/549b02898dd7c0541957659da8c56e93129507df)]: - - @segment/analytics-signals@1.0.1 - -## null - -### Patch Changes - -- Updated dependencies [[`7aed96e`](https://github.com/segmentio/analytics-next/commit/7aed96eac40a83bd392daa91838ed1f46e2dc9fd), [`d98dcd2`](https://github.com/segmentio/analytics-next/commit/d98dcd2f16aa8a8940e72fde0ba75d7974fe45fa), [`6bfaa3e`](https://github.com/segmentio/analytics-next/commit/6bfaa3e9d9ca767f54bb8185744e94be08ce9bc8), [`17432de`](https://github.com/segmentio/analytics-next/commit/17432de7b09d543c29f12c48ea61edf73aa7f4a1)]: - - @segment/analytics-next@1.72.2 - - @segment/analytics-signals@1.0.0 - -## null - -### Patch Changes - -- Updated dependencies [[`0fdf170`](https://github.com/segmentio/analytics-next/commit/0fdf1704af80c168113733beac3ef4eedeab6d2b)]: - - @segment/analytics-signals@0.1.1 - -## null - -### Patch Changes - -- Updated dependencies [[`73ac593`](https://github.com/segmentio/analytics-next/commit/73ac593226159423b2f63cac190eebd347bbb75a), [`2d89b1d`](https://github.com/segmentio/analytics-next/commit/2d89b1db2413d5c38f6fdb4832d111cd9141a51e), [`8d06af2`](https://github.com/segmentio/analytics-next/commit/8d06af29658b579e347ee8dbe39d6f62f01eab05), [`73ac593`](https://github.com/segmentio/analytics-next/commit/73ac593226159423b2f63cac190eebd347bbb75a), [`1f68f0e`](https://github.com/segmentio/analytics-next/commit/1f68f0e3309e291fb37f3732d8c32bd55f526633)]: - - @segment/analytics-next@1.72.1 - - @segment/analytics-signals@0.1.0 - -## null - -### Patch Changes - -- Updated dependencies [[`91e72ba`](https://github.com/segmentio/analytics-next/commit/91e72ba302fc45b4adb7aaeeb0a1f4ce3582dda6)]: - - @segment/analytics-next@1.72.0 - - @segment/analytics-signals@0.0.1 - -## null - -### Patch Changes - -- Updated dependencies [[`aee18d2`](https://github.com/segmentio/analytics-next/commit/aee18d222ddfb2273399987fabf92b54876f5e88), [`aee18d2`](https://github.com/segmentio/analytics-next/commit/aee18d222ddfb2273399987fabf92b54876f5e88), [`e60f625`](https://github.com/segmentio/analytics-next/commit/e60f6252687d977b76b09ca9b756c790d341111a), [`aee18d2`](https://github.com/segmentio/analytics-next/commit/aee18d222ddfb2273399987fabf92b54876f5e88)]: - - @segment/analytics-next@1.71.0 - - @segment/analytics-signals@0.0.1 diff --git a/packages/signals/signals-example/README.md b/packages/signals/signals-example/README.md deleted file mode 100644 index 6e54a223f..000000000 --- a/packages/signals/signals-example/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Signals Example - -### Instructions -- `cd packages/signals/signal-example` -- `yarn install` -- `yarn . build` -- Rename: `.env.example` -> `.env` (set `WRITEKEY=XXX` in `.env`) -- `yarn dev` -- If you make a change on another package that is not showing up, you can run `yarn . build` to rebuild all of the example's dependencies. \ No newline at end of file diff --git a/packages/signals/signals-example/package.json b/packages/signals/signals-example/package.json deleted file mode 100644 index 4e445df68..000000000 --- a/packages/signals/signals-example/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "@internal/signals-example", - "private": true, - "scripts": { - ".": "yarn run -T turbo run --filter=@internal/signals-example...", - "dev": "webpack serve", - "build": "webpack" - }, - "dependencies": { - "@segment/analytics-next": "workspace:^", - "@segment/analytics-signals": "workspace:^", - "react": "^18.0.0", - "react-dom": "^18.0.0", - "react-hook-form": "^7.54.2", - "react-router-dom": "^6.23.1" - }, - "devDependencies": { - "@babel/core": "^7.22.11", - "@babel/preset-env": "^7.22.10", - "@babel/preset-react": "^7.24.6", - "@babel/preset-typescript": "^7.22.11", - "@types/react": "^18.0.0", - "@types/react-dom": "^18", - "babel-loader": "^8.0.0", - "css-loader": "^7.1.2", - "dotenv-webpack": "^8.1.0", - "html-webpack-plugin": "^5.6.0", - "style-loader": "^4.0.0", - "typescript": "^4.7.0", - "webpack": "^5.94.0", - "webpack-cli": "^4.8.0", - "webpack-dev-server": "^4.15.1" - } -} diff --git a/packages/signals/signals-example/public/index.html b/packages/signals/signals-example/public/index.html deleted file mode 100644 index d34c48e56..000000000 --- a/packages/signals/signals-example/public/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - React TypeScript App - - - - -
- - - diff --git a/packages/signals/signals-example/src/components/App.tsx b/packages/signals/signals-example/src/components/App.tsx deleted file mode 100644 index df7fcb8e5..000000000 --- a/packages/signals/signals-example/src/components/App.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import '../lib/analytics' -import React, { useEffect } from 'react' -import { loadAnalytics } from '../lib/analytics' -import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom' -import { HomePage } from '../pages/Home' -import { OtherPage } from '../pages/Other' -import { ReactHookFormPage } from '../pages/ReactHookForm' - -const App: React.FC = () => { - useEffect(() => { - void loadAnalytics() - }, []) - - return ( - - - - } /> - } /> - } /> - - - ) -} - -export default App diff --git a/packages/signals/signals-example/src/components/ComplexForm.tsx b/packages/signals/signals-example/src/components/ComplexForm.tsx deleted file mode 100644 index f808ca953..000000000 --- a/packages/signals/signals-example/src/components/ComplexForm.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { FormEventHandler, useState } from 'react' - -const ComplexForm = () => { - const [inputField, setInputField] = useState('') - const [expectFormError, setExpectFormError] = useState(false) - const [selectField, setSelectField] = useState('') - - const statusCode: number = React.useMemo(() => { - try { - // Change the response status code via the text input field - const val = parseInt(inputField, 10) - return val >= 100 && val <= 511 ? val : 400 - } catch (err) { - return 400 - } - }, [inputField]) - - const handleSubmit: FormEventHandler = (event) => { - event.preventDefault() - - const formData = { - status: expectFormError ? statusCode : 200, - inputField, - selectField, - } - console.log('Submitting form:', JSON.stringify(formData)) - fetch('/parrot', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(formData), - }) - .then((response) => response.json()) - .then((data) => { - console.log('Form submitted successfully:', data) - }) - .catch((error) => { - console.error('Error submitting form:', error) - }) - console.log({ inputField, selectField }) - } - - return ( -
- -
-
- - setInputField(e.target.value)} - /> -
-
- - -
-
- - setExpectFormError(e.target.checked)} - /> -
- -
- -
- ) -} - -export default ComplexForm diff --git a/packages/signals/signals-example/src/components/ComplexReactHookForm.tsx b/packages/signals/signals-example/src/components/ComplexReactHookForm.tsx deleted file mode 100644 index 2e26d5f22..000000000 --- a/packages/signals/signals-example/src/components/ComplexReactHookForm.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react' -import { useForm, SubmitHandler } from 'react-hook-form' - -interface IFormInput { - name: string - selectField: string - expectFormError: boolean -} - -const ComplexFormWithHookForm = () => { - const { - register, - handleSubmit, - watch, - formState: { errors }, - } = useForm() - const onSubmit: SubmitHandler = (data) => { - const statusCode = data.expectFormError ? parseInt(data.name, 10) : 200 - const formData = { - status: statusCode, - name: data.name, - selectField: data.selectField, - } - console.log('Submitting form:', JSON.stringify(formData)) - fetch('/parrot', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(formData), - }) - .then((response) => response.json()) - .then((data) => { - console.log('Form submitted successfully:', data) - }) - .catch((error) => { - console.error('Error submitting form:', error) - }) - } - - const name = watch('name') - const statusCode = React.useMemo(() => { - try { - const val = parseInt(name, 10) - return val >= 100 && val <= 511 ? val : 400 - } catch (err) { - return 400 - } - }, [name]) - - return ( -
- -
-
- - - {errors.name && This field is required} -
-
- - - {errors.selectField && This field is required} -
-
- - -
- -
- -
- ) -} - -export default ComplexFormWithHookForm diff --git a/packages/signals/signals-example/src/index.tsx b/packages/signals/signals-example/src/index.tsx deleted file mode 100644 index 4cfb4d174..000000000 --- a/packages/signals/signals-example/src/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import App from './components/App' -import './styles.css' - -ReactDOM.render(, document.getElementById('root')) diff --git a/packages/signals/signals-example/src/lib/analytics.ts b/packages/signals/signals-example/src/lib/analytics.ts deleted file mode 100644 index bd591b41b..000000000 --- a/packages/signals/signals-example/src/lib/analytics.ts +++ /dev/null @@ -1,64 +0,0 @@ -// This is a fully client-side implementation example, so this file will never be run in a node environment -// You only want to instantiate SignalsPlugin in a browser context, otherwise you'll get an error. - -import { AnalyticsBrowser } from '@segment/analytics-next' -import { SignalsPlugin, ProcessSignal } from '@segment/analytics-signals' - -export const analytics = new AnalyticsBrowser() -if (!process.env.WRITEKEY) { - throw new Error('No writekey provided.') -} - -const processSignalExample: ProcessSignal = ( - signal, - { analytics, signals } -) => { - if (signal.type === 'interaction') { - const eventName = signal.data.eventType + ' ' + '[' + signal.type + ']' - analytics.track(eventName, signal.data) - } else if (signal.type === 'instrumentation') { - const found = signals.find( - signal, - 'interaction', - (s) => s.data.eventType === 'change' - ) - if (found) { - console.log('found in the buffer!', found.data) - analytics.track('found in the buffer!', found.data) - } - } -} - -const isStage = process.env.STAGE === 'true' - -const signalsPlugin = new SignalsPlugin({ - ...(isStage ? { apiHost: 'signals.segment.build/v1' } : {}), - sandboxStrategy: 'global', - // processSignal: processSignalExample, -}) - -export const loadAnalytics = () => - analytics - .load( - { - writeKey: process.env.WRITEKEY!, - plugins: [signalsPlugin], - ...(isStage ? { cdnURL: 'https://cdn.segment.build' } : {}), - }, - { - ...(isStage - ? { - integrations: { - 'Segment.io': { - apiHost: 'api.segment.build/v1', - }, - }, - } - : {}), - } - ) - .then(() => { - console.log(`Analytics loaded with WRITEKEY=${process.env.WRITEKEY}`) - // @ts-ignore - window.analytics = analytics // for debugging - }) diff --git a/packages/signals/signals-example/src/pages/Home.tsx b/packages/signals/signals-example/src/pages/Home.tsx deleted file mode 100644 index c17bbc2b0..000000000 --- a/packages/signals/signals-example/src/pages/Home.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import ComplexForm from '../components/ComplexForm' -import { analytics } from '../lib/analytics' - -export const HomePage: React.FC = () => { - return ( -
-

Hello, React with TypeScript!

- - -
- ) -} diff --git a/packages/signals/signals-example/src/pages/Other.tsx b/packages/signals/signals-example/src/pages/Other.tsx deleted file mode 100644 index 534eafd5c..000000000 --- a/packages/signals/signals-example/src/pages/Other.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' - -export const OtherPage: React.FC = () => { - React.useEffect(() => { - document.title = 'Other Page' - }, []) - - return ( -
-

Hello, I'm another page

- Go to Section -
-

A section

-

{`Lorem ipsum dolor sit amet`}

-
- Go to Top -
- ) -} diff --git a/packages/signals/signals-example/src/pages/ReactHookForm.tsx b/packages/signals/signals-example/src/pages/ReactHookForm.tsx deleted file mode 100644 index 6be232e11..000000000 --- a/packages/signals/signals-example/src/pages/ReactHookForm.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import ComplexForm from '../components/ComplexReactHookForm' -import { analytics } from '../lib/analytics' - -export const ReactHookFormPage: React.FC = () => { - return ( -
-

Hello, React Hook Form with TypeScript!

- - -
- ) -} diff --git a/packages/signals/signals-example/src/styles.css b/packages/signals/signals-example/src/styles.css deleted file mode 100644 index af0471def..000000000 --- a/packages/signals/signals-example/src/styles.css +++ /dev/null @@ -1,46 +0,0 @@ -main { - width: 960px; - margin: auto; -} - -form { - display: flex; - flex-direction: column; - max-width: 300px; - margin: 0 auto; -} - -label { - margin-bottom: 10px; -} - -input, -select { - width: 100%; - padding: 10px; - margin-top: 5px; -} - -button { - padding: 10px; - cursor: pointer; -} -nav { - background-color: #f8f9fa; - padding: 10px 0; - margin-bottom: px; -} - -nav a { - margin: 0 10px; - text-decoration: none; - color: #333; -} - -nav a:hover { - color: #007bff; -} - -form div { - display: inline-block; -} \ No newline at end of file diff --git a/packages/signals/signals-example/tsconfig.json b/packages/signals/signals-example/tsconfig.json deleted file mode 100644 index b668af3ef..000000000 --- a/packages/signals/signals-example/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "compilerOptions": { - "target": "ES6", - "module": "commonjs", - "jsx": "react", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "outDir": "./dist" - }, - "include": ["src"] -} diff --git a/packages/signals/signals-example/webpack.config.js b/packages/signals/signals-example/webpack.config.js deleted file mode 100644 index 3ad742b6c..000000000 --- a/packages/signals/signals-example/webpack.config.js +++ /dev/null @@ -1,65 +0,0 @@ -const path = require('path') -const HtmlWebpackPlugin = require('html-webpack-plugin') -const Dotenv = require('dotenv-webpack') -const bodyParser = require('body-parser') - -/** - * This is a base webpack config that is used for all generic web packages. - * It should contain the same support as analytics.js (e.g. es5, minified, etc) - * - * - * @type { import('webpack').Configuration } - */ - -module.exports = { - stats: 'minimal', - performance: { - hints: false, - }, - entry: './src/index.tsx', - devtool: 'source-map', - mode: 'development', - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'bundle.js', - clean: true, - }, - resolve: { - extensions: ['.ts', '.tsx', '.js'], - }, - module: { - rules: [ - { - test: /\.(ts|tsx)$/, - use: 'babel-loader', - exclude: /node_modules/, - }, - { - test: /\.css$/i, - use: ['style-loader', 'css-loader'], - }, - ], - }, - plugins: [ - new HtmlWebpackPlugin({ - template: './public/index.html', - }), - new Dotenv(), - ], - devServer: { - static: { - directory: path.join(__dirname, 'dist'), - }, - compress: true, - port: 9000, - hot: false, - onBeforeSetupMiddleware(devServer) { - devServer.app.use(bodyParser.json()) - devServer.app.post('/parrot', (req, res) => { - console.log('/parrot', req.body) - res.status(req.body.status ?? 200).json(req.body) - }) - }, - historyApiFallback: true, - }, -} diff --git a/packages/signals/signals-integration-tests/.babelrc b/packages/signals/signals-integration-tests/.babelrc deleted file mode 100644 index 701fada78..000000000 --- a/packages/signals/signals-integration-tests/.babelrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "presets": [ - "@babel/preset-env", - "@babel/preset-react", - "@babel/preset-typescript" - ] -} \ No newline at end of file diff --git a/packages/signals/signals-integration-tests/.eslintrc.js b/packages/signals/signals-integration-tests/.eslintrc.js deleted file mode 100644 index eedf2f3c3..000000000 --- a/packages/signals/signals-integration-tests/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type { import('eslint').Linter.Config } */ -module.exports = { - extends: ['../../../.eslintrc'], - env: { - browser: true, - }, -} diff --git a/packages/signals/signals-integration-tests/.lintstagedrc.js b/packages/signals/signals-integration-tests/.lintstagedrc.js deleted file mode 100644 index bc1f1c780..000000000 --- a/packages/signals/signals-integration-tests/.lintstagedrc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@internal/config").lintStagedConfig diff --git a/packages/signals/signals-integration-tests/jest.config.js b/packages/signals/signals-integration-tests/jest.config.js deleted file mode 100644 index 68ccd2e05..000000000 --- a/packages/signals/signals-integration-tests/jest.config.js +++ /dev/null @@ -1,3 +0,0 @@ -const { createJestTSConfig } = require('@internal/config') - -module.exports = createJestTSConfig(__dirname) diff --git a/packages/signals/signals-integration-tests/package.json b/packages/signals/signals-integration-tests/package.json deleted file mode 100644 index 17b92ea26..000000000 --- a/packages/signals/signals-integration-tests/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@internal/signals-integration-tests", - "version": "0.0.0", - "private": true, - "installConfig": { - "hoistingLimits": "workspaces" - }, - "scripts": { - ".": "yarn run -T turbo run --filter=@internal/signals-integration-tests...", - "build": "webpack", - "test:int": "playwright test && SKIP_BUILD=true yarn test:global-sandbox", - "test:vanilla": "playwright test src/tests/signals-vanilla", - "test:perf": "playwright test src/tests/performance", - "test:custom": "playwright test src/tests/custom", - "test:global-sandbox": "SANDBOX_STRATEGY=global playwright test src/tests/signals-vanilla src/tests/custom", - "watch": "webpack -w", - "lint": "yarn concurrently 'yarn:eslint .' 'yarn:tsc --noEmit'", - "concurrently": "yarn run -T concurrently", - "watch:test": "yarn test --watch", - "tsc": "yarn run -T tsc", - "eslint": "yarn run -T eslint", - "server": "http-server --port 5432", - "browser": "playwright test --debug" - }, - "packageManager": "yarn@3.4.1", - "devDependencies": { - "@internal/config": "workspace:^", - "@internal/test-helpers": "workspace:^", - "@playwright/test": "^1.28.1", - "@segment/analytics-next": "workspace:^", - "@segment/analytics-signals": "workspace:^", - "@types/react": "^18.0.0", - "@types/react-dom": "^18", - "globby": "^11.0.2", - "http-server": "14.1.1", - "react": "^18.0.0", - "react-aria-components": "^1.5.0", - "react-dom": "^18.0.0", - "tslib": "^2.4.1", - "webpack": "^5.94.0", - "webpack-cli": "^4.8.0" - } -} diff --git a/packages/signals/signals-integration-tests/playwright.config.ts b/packages/signals/signals-integration-tests/playwright.config.ts deleted file mode 100644 index 4ba30e097..000000000 --- a/packages/signals/signals-integration-tests/playwright.config.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { PlaywrightTestConfig } from '@playwright/test' -import { devices } from '@playwright/test' -import path from 'path' - -/** - * See https://playwright.dev/docs/test-configuration. - */ -const config: PlaywrightTestConfig = { - webServer: { - command: 'yarn run server', - url: 'http://127.0.0.1:5432', - reuseExistingServer: !process.env.CI, - }, - testDir: './src', - globalSetup: path.resolve(__dirname, 'playwright.global-setup.ts'), - /* Maximum time one test can run for. */ - timeout: 30 * 1000, - expect: { - /** - * Maximum time expect() should wait for the condition to be met. - * For example in `await expect(locator).toHaveText();` - */ - timeout: 5000, - }, - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [['html', { open: 'never' }]], - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on', - launchOptions: { - args: ['--enable-precise-memory-info', '--js-flags=--expose-gc'], - }, - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { - ...devices['Desktop Chrome'], - }, - }, - ], -} - -export default config diff --git a/packages/signals/signals-integration-tests/playwright.global-setup.ts b/packages/signals/signals-integration-tests/playwright.global-setup.ts deleted file mode 100644 index 2fb62b2f9..000000000 --- a/packages/signals/signals-integration-tests/playwright.global-setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { FullConfig } from '@playwright/test' -import { execSync } from 'child_process' -import { envConfig } from './src/helpers/env-config' - -export default function globalSetup(_cfg: FullConfig) { - console.log(`Executing playwright.global-setup.ts...\n`) - console.log(`Using envConfig: ${JSON.stringify(envConfig, undefined, 2)}\n`) - if (process.env.SKIP_BUILD !== 'true') { - console.log(`Executing yarn build:\n`) - execSync('yarn build', { stdio: 'inherit' }) - } - console.log('Finished global setup. Should start running tests.\n') -} 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 deleted file mode 100644 index 13960dec6..000000000 --- a/packages/signals/signals-integration-tests/src/helpers/base-page-object.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { CDNSettingsBuilder } from '@internal/test-helpers' -import { Page } from '@playwright/test' -import { logConsole } from './log-console' -import { Signal, SignalsPluginSettingsConfig } from '@segment/analytics-signals' -import { - PageNetworkUtils, - SignalAPIRequestBuffer, - TrackingAPIRequestBuffer, -} from './network-utils' -import { envConfig } from './env-config' - -export class BasePage { - protected page!: Page - public signalsAPI = new SignalAPIRequestBuffer() - public trackingAPI = new TrackingAPIRequestBuffer() - public url: string - public edgeFnDownloadURL = 'https://cdn.edgefn.segment.com/MY-WRITEKEY/foo.js' - public edgeFn!: string - public network!: PageNetworkUtils - public defaultSignalsPluginTestSettings: Partial = - { - disableSignalsRedaction: true, - enableSignalsIngestion: true, - flushInterval: 500, - } - - constructor(path: string) { - this.url = 'http://localhost:5432/src/tests' + path - } - - public origin() { - return new URL(this.page.url()).origin - } - - /** - * Load and setup routes - * and wait for analytics and signals to be initialized - */ - async loadAndWait(...args: Parameters) { - await Promise.all([this.load(...args), this.waitForSettings()]) - return this - } - - /** - * load and setup routes - */ - async load( - page: Page, - edgeFn: string, - signalSettings: Partial = {}, - options: { - updateURL?: (url: string) => string - sampleRate?: number - middleware?: string - skipSignalsPluginInit?: boolean - } = {} - ) { - logConsole(page) - this.page = page - this.network = new PageNetworkUtils(page) - this.edgeFn = edgeFn - await this.setupMockedRoutes(options.sampleRate) - const url = options.updateURL ? options.updateURL(this.url) : this.url - await this.page.goto(url, { waitUntil: 'domcontentloaded' }) - if (!options.skipSignalsPluginInit) { - void this.invokeAnalyticsLoad({ - sandboxStrategy: envConfig.SANDBOX_STRATEGY, - flushInterval: 500, - ...signalSettings, - }) - } - return this - } - - /** - * Wait for analytics and signals to be initialized - * We could do the same thing with analytics.ready() and signalsPlugin.ready() - */ - async waitForSettings() { - return Promise.all([ - this.waitForCDNSettingsResponse(), - this.waitForEdgeFunctionResponse(), - ]) - } - - /** - * Invoke the analytics load sequence, but do not wait for analytics to full initialize - * Full initialization means that the CDN settings and edge function have been loaded - */ - private async invokeAnalyticsLoad( - signalSettings: Partial = {} - ) { - await this.page.evaluate( - ({ settings }) => { - window.signalsPlugin = new window.SignalsPlugin(settings) - window.analytics.load({ - writeKey: '', - plugins: [window.signalsPlugin], - }) - }, - { - settings: { - ...this.defaultSignalsPluginTestSettings, - ...signalSettings, - }, - } - ) - return this - } - - private async setupMockedRoutes(sampleRate?: number) { - // clear any existing saved requests - this.trackingAPI.clear() - this.signalsAPI.clear() - - await Promise.all([ - this.mockSignalsApi(), - this.mockTrackingApi(), - this.mockCDNSettings(sampleRate), - ]) - } - - async mockTrackingApi() { - await this.page.route('https://api.segment.io/v1/*', (route, request) => { - if (request.method().toLowerCase() !== 'post') { - throw new Error(`Unexpected method: ${request.method()}`) - } - this.trackingAPI.addRequest(request) - return route.fulfill({ - contentType: 'application/json', - status: 201, - body: JSON.stringify({ - success: true, - }), - }) - }) - } - - waitForTrackingApiFlush(timeout = 5000) { - return this.page.waitForResponse('https://api.segment.io/v1/*', { timeout }) - } - - async mockSignalsApi() { - await this.page.route( - 'https://signals.segment.io/v1/*', - (route, request) => { - if (request.method().toLowerCase() !== 'post') { - throw new Error(`Unexpected method: ${request.method()}`) - } - this.signalsAPI.addRequest(request) - return route.fulfill({ - contentType: 'application/json', - status: 201, - body: JSON.stringify({ - success: true, - }), - }) - } - ) - } - - async waitForSignalsEmit( - filter: (signal: Signal) => boolean, - { - expectedSignalCount, - maxTimeoutMs = 10000, - failOnEmit = false, - }: { - expectedSignalCount?: number - maxTimeoutMs?: number - failOnEmit?: boolean - } = {} - ) { - return this.page.evaluate( - ([filter, expectedSignalCount, maxTimeoutMs, failOnEmit]) => { - return new Promise((resolve, reject) => { - let signalCount = 0 - const to = setTimeout(() => { - if (failOnEmit) { - resolve('No signal emitted') - } else { - reject('Timed out waiting for signals') - } - }, maxTimeoutMs) - window.signalsPlugin.onSignal((signal) => { - signalCount++ - if ( - eval(filter)(signal) && - signalCount === (expectedSignalCount ?? 1) - ) { - if (failOnEmit) { - reject( - `Signal should not have been emitted: ${JSON.stringify( - signal, - null, - 2 - )}` - ) - } else { - resolve(signal) - } - clearTimeout(to) - } - }) - }) - }, - [ - filter.toString(), - expectedSignalCount, - maxTimeoutMs, - failOnEmit, - ] as const - ) - } - - waitForSignalsApiFlush(timeout = 5000) { - return this.page.waitForResponse('https://signals.segment.io/v1/*', { - timeout, - }) - } - - async mockCDNSettings(sampleRate?: number) { - await this.page.route( - 'https://cdn.segment.com/v1/projects/*/settings', - (route, request) => { - if (request.method().toLowerCase() !== 'get') { - throw new Error('expect to be a GET request') - } - - const cdnSettings = new CDNSettingsBuilder({ - writeKey: '', - baseCDNSettings: { - edgeFunction: { - downloadURL: this.edgeFnDownloadURL, - version: 1, - }, - autoInstrumentationSettings: { - sampleRate: sampleRate ?? 1, - }, - }, - }).build() - - return route.fulfill({ - status: 200, - contentType: 'application/json', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(cdnSettings), - }) - } - ) - - await this.page.route(this.edgeFnDownloadURL, (route, request) => { - if (request.method().toLowerCase() !== 'get') { - throw new Error('expect to be a GET request') - } - - // Mock response for the edge function download URL - // This can be customized based on the test requirements - return route.fulfill({ - status: 200, - contentType: 'application/javascript', - body: this.edgeFn, - }) - }) - } - - waitForEdgeFunctionResponse(timeout = 30000) { - return this.page.waitForResponse(this.edgeFnDownloadURL, { - timeout, - }) - } - - async waitForCDNSettingsResponse(timeout = 30000) { - return this.page.waitForResponse( - 'https://cdn.segment.com/v1/projects/*/settings', - { timeout } - ) - } -} diff --git a/packages/signals/signals-integration-tests/src/helpers/env-config.ts b/packages/signals/signals-integration-tests/src/helpers/env-config.ts deleted file mode 100644 index 880918dc6..000000000 --- a/packages/signals/signals-integration-tests/src/helpers/env-config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { SignalsPluginSettingsConfig } from '@segment/analytics-signals' - -// This is for testing with the global sandbox strategy with an npm script, that executes processSignal in the global scope -// If we change this to be the default, this can be rejiggered -const SANDBOX_STRATEGY = (process.env.SANDBOX_STRATEGY ?? - 'iframe') as SignalsPluginSettingsConfig['sandboxStrategy'] - -export const envConfig = { - SANDBOX_STRATEGY, -} diff --git a/packages/signals/signals-integration-tests/src/helpers/fixtures.ts b/packages/signals/signals-integration-tests/src/helpers/fixtures.ts deleted file mode 100644 index 58494cafe..000000000 --- a/packages/signals/signals-integration-tests/src/helpers/fixtures.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { PageData } from '@segment/analytics-signals-runtime' - -const pageData: PageData = { - hash: '', - hostname: 'localhost', - path: '/src/tests/signals-vanilla/index.html', - referrer: '', - search: '', - title: '', - url: 'http://localhost:5432/src/tests/signals-vanilla/index.html', -} - -export const commonSignalData = { - page: pageData, -} diff --git a/packages/signals/signals-integration-tests/src/helpers/log-console.ts b/packages/signals/signals-integration-tests/src/helpers/log-console.ts deleted file mode 100644 index 93c2e12f8..000000000 --- a/packages/signals/signals-integration-tests/src/helpers/log-console.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Page } from '@playwright/test' - -export const logConsole = (page: Page) => { - page.on('console', (msg) => { - const text = msg.text() - // keep stdout clean, e.g. by not printing intentional errors - const ignoreList = ['Bad Request'] - if (ignoreList.some((str) => text.includes(str))) { - return - } - console.log(`console.${msg.type()}:`, text) - }) - page.on('pageerror', (error) => { - console.error('Page error:', error) - }) -} diff --git a/packages/signals/signals-integration-tests/src/helpers/network-utils.ts b/packages/signals/signals-integration-tests/src/helpers/network-utils.ts deleted file mode 100644 index b9f7e9e57..000000000 --- a/packages/signals/signals-integration-tests/src/helpers/network-utils.ts +++ /dev/null @@ -1,232 +0,0 @@ -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 { - method?: string - body?: any - contentType?: string - responseType?: XMLHttpRequestResponseType - responseLatency?: number -} -export class PageNetworkUtils { - private defaultTestApiURL = 'http://localhost:5432/api/foo' - private defaultResponseTimeout = 3000 - constructor(public page: Page) {} - - async makeXHRCall( - url = this.defaultTestApiURL, - reqOptions: XHRRequestOptions = {} - ): Promise { - let normalizeUrl = url - if (url.startsWith('/')) { - normalizeUrl = new URL(url, this.page.url()).href - } - const req = this.page.waitForResponse(normalizeUrl ?? url, { - timeout: this.defaultResponseTimeout, - }) - const responseBody = this.page.evaluate( - (args) => { - return new Promise((resolve) => { - const xhr = new XMLHttpRequest() - xhr.open(args.method ?? 'POST', args.url) - - const contentType = args.contentType ?? 'application/json' - xhr.setRequestHeader('Content-Type', contentType) - - xhr.responseType = args.responseType - ? args.responseType - : contentType.includes('json') - ? 'json' - : '' // '' is the same as 'text' according to xhr spec - - if (typeof args.responseLatency === 'number') { - xhr.setRequestHeader( - 'x-test-latency', - args.responseLatency.toString() - ) - } - xhr.send(args.body ?? JSON.stringify({ foo: 'bar' })) - xhr.onload = () => resolve(xhr.response) - }) - }, - { url, ...reqOptions } - ) - await req - return responseBody - } - async makeFileUploadRequest(url: string) { - let normalizeUrl = url - if (url.startsWith('/')) { - normalizeUrl = new URL(url, this.page.url()).href - } - const req = this.page.waitForResponse(normalizeUrl ?? url, { - timeout: this.defaultResponseTimeout, - }) - await this.page.evaluate((_url) => { - const formData = new FormData() - const file = new File(['file content'], 'test.txt', { - type: 'text/plain', - }) - formData.append('file', file) - return fetch(_url, { - method: 'POST', - body: formData, - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) - }, normalizeUrl) - await req - } - /** - * Make a fetch call in the page context. By default it will POST a JSON object with {foo: 'bar'} - */ - async makeFetchCall( - url = this.defaultTestApiURL, - request: Partial & { - contentType?: string - blob?: boolean - } = {} - ): Promise { - let normalizeUrl = url - if (url.startsWith('/')) { - normalizeUrl = new URL(url, this.page.url()).href - } - const req = this.page.waitForResponse(normalizeUrl ?? url, { - timeout: this.defaultResponseTimeout, - }) - const responseBody = await this.page.evaluate( - async (args) => { - const res = await fetch(args.url, { - method: 'POST', - headers: { - 'Content-Type': args.request.contentType ?? 'application/json', - }, - body: JSON.stringify({ foo: 'bar' }), - ...args.request, - }) - const type = res.headers.get('Content-Type') - if (type?.includes('json')) { - return res.json() - } else if (type?.includes('text')) { - return res.text() - } else { - console.error('Unexpected response content type') - } - }, - { url, request } - ) - await req - return responseBody - } - - async mockTestRoute( - url = this.defaultTestApiURL, - response?: Partial - ) { - if (url.startsWith('/')) { - url = new URL(url, this.page.url()).href - } - await this.page.route(url, async (route) => { - const latency = this.extractLatency(route) - - // if a custom latency is set in the request headers, use that instead - - await new Promise((resolve) => setTimeout(resolve, latency)) - return route.fulfill({ - contentType: 'application/json', - status: 200, - body: JSON.stringify({ someResponse: 'yep' }), - ...response, - }) - }) - } - private extractLatency(route: Route) { - let latency = 0 - if (route.request().headers()['x-test-latency']) { - const customLatency = parseInt( - route.request().headers()['x-test-latency'] - ) - if (customLatency) { - latency = customLatency - } - } - return latency - } -} - -export class TrackingAPIRequestBuffer { - private requests: Request[] = [] - public lastEvent(): SegmentEvent { - const allEvents = this.getEvents() - return allEvents[allEvents.length - 1] - } - public getEvents(): SegmentEvent[] { - return this.requests.flatMap((req) => { - const body = req.postDataJSON() - return 'batch' in body ? body.batch : [body] - }) - } - - clear() { - this.requests = [] - } - - addRequest(request: Request) { - if (request.method().toLowerCase() !== 'post') { - throw new Error( - `Unexpected method: ${request.method()}, Tracking API only accepts POST` - ) - } - this.requests.push(request) - } -} - -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) - } - return super.getEvents() - } - - override lastEvent(signalType?: Signal['type']): SegmentEvent { - if (signalType) { - const res = - this.getEvents(signalType)[this.getEvents(signalType).length - 1] - if (!res) { - throw new Error(`No signal of type ${signalType} found`) - } - return res - } - return super.lastEvent() - } -} diff --git a/packages/signals/signals-integration-tests/src/helpers/playwright-utils.ts b/packages/signals/signals-integration-tests/src/helpers/playwright-utils.ts deleted file mode 100644 index f06bb8333..000000000 --- a/packages/signals/signals-integration-tests/src/helpers/playwright-utils.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Page } from '@playwright/test' -import type { Compute } from './ts' - -export function waitForCondition( - conditionFn: () => boolean, - { - checkInterval = 100, - timeout = 10000, - errorMessage = 'Condition was not met within the specified time.', - } = {} -): Promise { - return new Promise((resolve, reject) => { - const startTime = Date.now() - - const interval = setInterval(() => { - try { - if (conditionFn()) { - clearInterval(interval) - resolve() - } else if (Date.now() - startTime >= timeout) { - clearInterval(interval) - reject(new Error(`${errorMessage}. Timeout: ${timeout}ms`)) - } - } catch (error) { - clearInterval(interval) - reject(error) - } - }, checkInterval) - }) -} - -type FillOptions = Compute[2]> - -export async function fillAndBlur( - page: Page, - selector: string, - text: string, - options: FillOptions = {} -) { - await page.fill(selector, text, options) - // Remove focus so the onChange event is triggered - await page.evaluate( - (args) => { - const input = document.querySelector(args.selector) as HTMLElement - if (input) { - input.blur() - } - }, - { selector } - ) -} diff --git a/packages/signals/signals-integration-tests/src/helpers/ts.ts b/packages/signals/signals-integration-tests/src/helpers/ts.ts deleted file mode 100644 index b68acdfa9..000000000 --- a/packages/signals/signals-integration-tests/src/helpers/ts.ts +++ /dev/null @@ -1 +0,0 @@ -export type Compute = { [K in keyof T]: T[K] } & {} diff --git a/packages/signals/signals-integration-tests/src/shims.d.ts b/packages/signals/signals-integration-tests/src/shims.d.ts deleted file mode 100644 index 5b1f53835..000000000 --- a/packages/signals/signals-integration-tests/src/shims.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { AnalyticsBrowser } from '@segment/analytics-next' -import type { SignalsPlugin } from '@segment/analytics-signals' - -declare global { - interface Window { - analytics: AnalyticsBrowser - signalsPlugin: SignalsPlugin - SignalsPlugin: typeof SignalsPlugin - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/App.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/App.tsx deleted file mode 100644 index 0f736a132..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/App.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react' -import { TextField } from './TextField' -import { Select, SelectItem } from './Select' - -export const App: React.FC = () => { - return ( -
-
-

TextField

- -
-
-

Select

- -
-
- ) -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.css deleted file mode 100644 index 6ce5422fe..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.css +++ /dev/null @@ -1,42 +0,0 @@ -@import "./theme.css"; - -.react-aria-Button { - color: var(--text-color); - background: var(--button-background); - border: 1px solid var(--border-color); - border-radius: 4px; - appearance: none; - vertical-align: middle; - font-size: 1rem; - text-align: center; - margin: 0; - outline: none; - padding: 6px 10px; - text-decoration: none; - - &[data-pressed] { - box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1); - background: var(--button-background-pressed); - border-color: var(--border-color-pressed); - } - - &[data-focus-visible] { - outline: 2px solid var(--focus-ring-color); - outline-offset: -1px; - } - - &[data-disabled]{ - border-color: var(--border-color-disabled); - color: var(--text-color-disabled); - } -} - -@keyframes toggle { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.tsx deleted file mode 100644 index 85824ea50..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import { Button as RACButton, ButtonProps } from 'react-aria-components' -import './Button.css' - -export function Button(props: ButtonProps) { - return -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.css deleted file mode 100644 index 47226a65e..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.css +++ /dev/null @@ -1,99 +0,0 @@ -@import "./theme.css"; - -.react-aria-Checkbox { - --selected-color: var(--highlight-background); - --selected-color-pressed: var(--highlight-background-pressed); - --checkmark-color: var(--highlight-foreground); - - display: flex; - align-items: center; - gap: 0.571rem; - font-size: 1.143rem; - color: var(--text-color); - forced-color-adjust: none; - - .checkbox { - width: 1.143rem; - height: 1.143rem; - border: 2px solid var(--border-color); - border-radius: 4px; - transition: all 200ms; - display: flex; - align-items: center; - justify-content: center; - } - - svg { - width: 1rem; - height: 1rem; - fill: none; - stroke: var(--checkmark-color); - stroke-width: 3px; - stroke-dasharray: 22px; - stroke-dashoffset: 66; - transition: all 200ms; - } - - &[data-pressed] .checkbox { - border-color: var(--border-color-pressed); - } - - &[data-focus-visible] .checkbox { - outline: 2px solid var(--focus-ring-color); - outline-offset: 2px; - } - - &[data-selected], - &[data-indeterminate] { - .checkbox { - border-color: var(--selected-color); - background: var(--selected-color); - } - - &[data-pressed] .checkbox { - border-color: var(--selected-color-pressed); - background: var(--selected-color-pressed); - } - - svg { - stroke-dashoffset: 44; - } - } - - &[data-indeterminate] { - & svg { - stroke: none; - fill: var(--checkmark-color); - } - } - - &[data-invalid] { - .checkbox { - --checkmark-color: var(--gray-50); - border-color: var(--invalid-color); - } - - &[data-pressed] .checkbox { - border-color: var(--invalid-color-pressed); - } - - &[data-selected], - &[data-indeterminate] { - .checkbox { - background: var(--invalid-color); - } - - &[data-pressed] .checkbox { - background: var(--invalid-color-pressed); - } - } - } - - &[data-disabled] { - color: var(--text-color-disabled); - - .checkbox { - border-color: var(--border-color-disabled); - } - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.tsx deleted file mode 100644 index a6aad2c68..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Checkbox.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Checkbox as AriaCheckbox, CheckboxProps } from 'react-aria-components' -import React from 'react' -import './Checkbox.css' - -export function Checkbox({ children, ...props }: CheckboxProps) { - return ( - - {({ isIndeterminate }) => ( - <> -
- -
- {children} - - )} -
- ) -} - -export { Checkbox as MyCheckbox } diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.css deleted file mode 100644 index 7377d73fc..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.css +++ /dev/null @@ -1,125 +0,0 @@ -@import './Checkbox.css'; -@import './ListBox.css'; -@import './Popover.css'; -@import './Form.css'; -@import './Button.css'; -@import "./theme.css"; - -.react-aria-ComboBox { - color: var(--text-color); - - .react-aria-Input { - margin: 0; - font-size: 1.072rem; - background: var(--field-background); - color: var(--field-text-color); - border: 1px solid var(--border-color); - border-radius: 6px; - padding: 0.286rem 2rem 0.286rem 0.571rem; - vertical-align: middle; - - &[data-focused] { - outline: none; - outline: 2px solid var(--focus-ring-color); - outline-offset: -1px; - } - } - - .react-aria-Button { - background: var(--highlight-background); - color: var(--highlight-foreground); - forced-color-adjust: none; - border-radius: 4px; - border: none; - margin-left: -1.714rem; - width: 1.429rem; - height: 1.429rem; - padding: 0; - font-size: 0.857rem; - cursor: default; - - &[data-pressed] { - box-shadow: none; - background: var(--highlight-background); - } - } -} - -.react-aria-Popover[data-trigger=ComboBox] { - width: var(--trigger-width); - - .react-aria-ListBox { - display: block; - width: unset; - max-height: inherit; - min-height: unset; - border: none; - - .react-aria-Header { - padding-left: 1.571rem; - } - } - - .react-aria-ListBoxItem { - padding: 0.286rem 0.571rem 0.286rem 1.571rem; - - &[data-focus-visible] { - outline: none; - } - - &[data-selected] { - font-weight: 600; - background: unset; - color: var(--text-color); - - &::before { - content: '✓'; - content: '✓' / ''; - alt: ' '; - position: absolute; - top: 4px; - left: 4px; - } - } - - &[data-focused], - &[data-pressed] { - background: var(--highlight-background); - color: var(--highlight-foreground); - } - } -} - -.react-aria-ListBoxItem[href] { - text-decoration: none; - cursor: pointer; -} - -.react-aria-ComboBox { - .react-aria-Input { - &[data-disabled] { - border-color: var(--border-color-disabled); - } - } - - .react-aria-Button { - &[data-disabled] { - background: var(--border-color-disabled); - } - } - - .react-aria-Input { - &[data-invalid]:not([data-focused]) { - border-color: var(--invalid-color); - } - } - - .react-aria-FieldError { - font-size: 12px; - color: var(--invalid-color); - } - - [slot=description] { - font-size: 12px; - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.tsx deleted file mode 100644 index 5d76259ab..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ComboBox.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { - Button, - ComboBox as AriaComboBox, - ComboBoxProps as AriaComboBoxProps, - FieldError, - Input, - Label, - ListBox, - ListBoxItem, - ListBoxItemProps, - Popover, - Text, - ValidationResult, -} from 'react-aria-components' -import React from 'react' - -import './ComboBox.css' - -export interface ComboBoxProps - extends Omit, 'children'> { - label?: string - description?: string | null - errorMessage?: string | ((validation: ValidationResult) => string) - children: React.ReactNode | ((item: T) => React.ReactNode) -} - -export function ComboBox({ - label, - description, - errorMessage, - children, - ...props -}: ComboBoxProps) { - return ( - - -
- - -
- {description && {description}} - {errorMessage} - - {children} - -
- ) -} - -export function ComboBoxItem(props: ListBoxItemProps) { - return -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.css deleted file mode 100644 index a5051aeb2..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.css +++ /dev/null @@ -1,17 +0,0 @@ -@import "./theme.css"; -@import './Button.css'; -@import './TextField.css'; -@import './Modal.css'; - -.react-aria-Dialog { - outline: none; - padding: 30px; - max-height: inherit; - box-sizing: border-box; - overflow: auto; - - .react-aria-Heading[slot=title] { - line-height: 1em; - margin-top: 0; - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.tsx deleted file mode 100644 index 40d5a58c2..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { Dialog as RACDialog, DialogProps } from 'react-aria-components' -import './Dialog.css' -import React from 'react' - -export function Dialog(props: DialogProps) { - return -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.css deleted file mode 100644 index 37c01bfd6..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.css +++ /dev/null @@ -1,32 +0,0 @@ -@import "./theme.css"; -@import './TextField.css'; -@import './Button.css'; - -.react-aria-Form { - display: flex; - flex-direction: column; - align-items: start; - gap: 8px; -} - -.react-aria-Form [role=alert] { - border: 2px solid var(--invalid-color); - background: var(--overlay-background); - border-radius: 6px; - padding: 12px; - max-width: 250px; - outline: none; - - &:focus-visible { - outline: 2px solid var(--focus-ring-color); - outline-offset: 2px; - } - - h3 { - margin-top: 0; - } - - p { - margin-bottom: 0; - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.tsx deleted file mode 100644 index 6b5d38d1a..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import { Form as RACForm, FormProps } from 'react-aria-components' -import './Form.css' - -export function Form(props: FormProps) { - return -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.css deleted file mode 100644 index 3784cbf7d..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.css +++ /dev/null @@ -1,223 +0,0 @@ -@import './Checkbox.css'; -@import "./theme.css"; - -.react-aria-ListBox { - display: flex; - flex-direction: column; - max-height: inherit; - overflow: auto; - padding: 2px; - border: 1px solid var(--border-color); - border-radius: 6px; - background: var(--overlay-background); - forced-color-adjust: none; - outline: none; - width: 250px; - max-height: 300px; - min-height: 100px; - box-sizing: border-box; - - &[data-focus-visible] { - outline: 2px solid var(--focus-ring-color); - outline-offset: -1px; - } -} - -.react-aria-ListBoxItem { - margin: 2px; - padding: 0.286rem 0.571rem; - border-radius: 6px; - outline: none; - cursor: default; - color: var(--text-color); - font-size: 1.072rem; - position: relative; - display: flex; - flex-direction: column; - - &[data-focus-visible] { - outline: 2px solid var(--focus-ring-color); - outline-offset: -2px; - } - - &[data-selected] { - background: var(--highlight-background); - color: var(--highlight-foreground); - - &[data-focus-visible] { - outline-color: var(--highlight-foreground); - outline-offset: -4px; - } - } -} - -.react-aria-ListBoxItem[href] { - text-decoration: none; - cursor: pointer; - -webkit-touch-callout: none; -} - -.react-aria-ListBox { - .react-aria-ListBoxSection:not(:first-child) { - margin-top: 12px; - } - - .react-aria-Header { - font-size: 1.143rem; - font-weight: bold; - padding: 0 0.714rem; - } -} - -.react-aria-ListBoxItem { - [slot=label] { - font-weight: bold; - } - - [slot=description] { - font-size: small; - } -} - -.react-aria-ListBox[data-orientation=horizontal], -.react-aria-ListBox[data-layout=grid] { - flex-direction: row; - width: fit-content; - max-width: 100%; - padding: 4px; - - .react-aria-ListBoxItem { - position: relative; - margin: 0; - padding: 4px; - - & img { - object-fit: cover; - aspect-ratio: 1/1; - max-width: 150px; - margin-bottom: 4px; - border-radius: 4px; - transition: box-shadow 200ms; - } - - &[data-hovered] { - & img { - box-shadow: 0 0 8px rgb(from slateblue r g b / 0.5); - } - } - - &[data-selected] { - background: none; - color: inherit; - - & img { - box-shadow: 0 0 12px rgb(from slateblue r g b / 0.8); - } - - &:after { - content: '✓'; - content: '✓' / ''; - alt: ' '; - position: absolute; - top: 8px; - right: 8px; - background: var(--highlight-background); - border: 2px solid var(--highlight-foreground); - color: var(--highlight-foreground); - width: 22px; - height: 22px; - border-radius: 22px; - box-sizing: border-box; - font-size: 14px; - line-height: 1em; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 0 8px rgb(0 0 0 / .5); - } - } - } -} - -.react-aria-ListBox[data-layout=grid] { - display: grid; - grid-template-columns: 1fr 1fr; - scrollbar-gutter: stable; -} - -.react-aria-ListBox[data-layout=grid][data-orientation=horizontal] { - width: 100%; - max-width: none; - display: grid; - grid-auto-flow: column; - grid-template-rows: 58px 58px; - grid-template-columns: none; - grid-auto-columns: 250px; - max-height: 200px; - gap: 8px; - - .react-aria-ListBoxItem { - display: grid; - grid-template-areas: "image ." - "image title" - "image description" - "image ."; - grid-template-columns: auto 1fr; - grid-template-rows: 1fr auto auto 1fr; - column-gap: 8px; - - & img { - width: 50px; - height: 50px; - grid-area: image; - margin-bottom: 0; - } - - [slot=label] { - grid-area: title; - } - - [slot=description] { - grid-area: description; - } - } -} - -.react-aria-ListBoxItem { - &[data-disabled] { - color: var(--text-color-disabled); - } -} - -.react-aria-ListBox { - &[data-empty] { - align-items: center; - justify-content: center; - font-style: italic; - } -} - -.react-aria-ListBoxItem { - &[data-dragging] { - opacity: 0.6; - } -} - -.react-aria-DropIndicator[data-drop-target] { - outline: 1px solid var(--highlight-background); -} - -.react-aria-ListBox[data-drop-target] { - outline: 2px solid var(--highlight-background); - outline-offset: -1px; - background: var(--highlight-overlay) -} - -.react-aria-ListBoxItem[data-drop-target] { - outline: 2px solid var(--highlight-background); - background: var(--highlight-overlay) -} - -.react-aria-DropIndicator[data-drop-target] { - outline: 1px solid var(--highlight-background); -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.tsx deleted file mode 100644 index 086b7d7e5..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react' -import { - ListBox as AriaListBox, - ListBoxItem as AriaListBoxItem, - ListBoxItemProps, - ListBoxProps, -} from 'react-aria-components' - -import './ListBox.css' - -export function ListBox({ - children, - ...props -}: ListBoxProps) { - return {children} -} - -export function ListBoxItem(props: ListBoxItemProps) { - return -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.css deleted file mode 100644 index f8fec1164..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.css +++ /dev/null @@ -1,84 +0,0 @@ -@import './Button.css'; -@import './TextField.css'; -@import "./theme.css"; - -.react-aria-ModalOverlay { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: var(--visual-viewport-height); - background: rgba(0 0 0 / .5); - display: flex; - align-items: center; - justify-content: center; - z-index: 100; - - &[data-entering] { - animation: modal-fade 200ms; - } - - &[data-exiting] { - animation: modal-fade 150ms reverse ease-in; - } -} - -.react-aria-Modal { - box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); - border-radius: 6px; - background: var(--overlay-background); - color: var(--text-color); - border: 1px solid var(--gray-400); - outline: none; - max-width: 300px; - - &[data-entering] { - animation: modal-zoom 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275); - } - - .react-aria-TextField { - margin-bottom: 8px; - } -} - -@keyframes modal-fade { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -@keyframes modal-zoom { - from { - transform: scale(0.8); - } - - to { - transform: scale(1); - } -} - -@keyframes mymodal-blur { - from { - background: rgba(45 0 0 / 0); - backdrop-filter: blur(0); - } - - to { - background: rgba(45 0 0 / .3); - backdrop-filter: blur(10px); - } -} - -@keyframes mymodal-slide { - from { - transform: translateX(100%); - } - - to { - transform: translateX(0); - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.tsx deleted file mode 100644 index 9cfce9871..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { Modal as RACModal, ModalOverlayProps } from 'react-aria-components' -import './Modal.css' -import React from 'react' - -export function Modal(props: ModalOverlayProps) { - return -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.css deleted file mode 100644 index fd060cd39..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.css +++ /dev/null @@ -1,87 +0,0 @@ -@import './Button.css'; -@import './Dialog.css'; -@import './Switch.css'; -@import "./theme.css"; - -.react-aria-Popover { - --background-color: var(--overlay-background); - - border: 1px solid var(--border-color); - box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); - border-radius: 6px; - background: var(--background-color); - color: var(--text-color); - outline: none; - max-width: 250px; - - .react-aria-OverlayArrow svg { - display: block; - fill: var(--background-color); - stroke: var(--border-color); - stroke-width: 1px; - } - - &[data-placement=top] { - --origin: translateY(8px); - - &:has(.react-aria-OverlayArrow) { - margin-bottom: 6px; - } - } - - &[data-placement=bottom] { - --origin: translateY(-8px); - - &:has(.react-aria-OverlayArrow) { - margin-top: 6px; - } - - .react-aria-OverlayArrow svg { - transform: rotate(180deg); - } - } - - &[data-placement=right] { - --origin: translateX(-8px); - - &:has(.react-aria-OverlayArrow) { - margin-left: 6px; - } - - .react-aria-OverlayArrow svg { - transform: rotate(90deg); - } - } - - &[data-placement=left] { - --origin: translateX(8px); - - &:has(.react-aria-OverlayArrow) { - margin-right: 6px; - } - - .react-aria-OverlayArrow svg { - transform: rotate(-90deg); - } - } - - &[data-entering] { - animation: popover-slide 200ms; - } - - &[data-exiting] { - animation: popover-slide 200ms reverse ease-in; - } -} - -@keyframes popover-slide { - from { - transform: var(--origin); - opacity: 0; - } - - to { - transform: translateY(0); - opacity: 1; - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.tsx deleted file mode 100644 index 622627563..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react' -import { - Dialog, - OverlayArrow, - Popover as AriaPopover, - PopoverProps as AriaPopoverProps, -} from 'react-aria-components' - -import './Popover.css' - -export interface PopoverProps extends Omit { - children: React.ReactNode -} - -export function Popover({ children, ...props }: PopoverProps) { - return ( - - - - - - - {children} - - ) -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.css deleted file mode 100644 index ec6765e2b..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.css +++ /dev/null @@ -1,139 +0,0 @@ -@import './ListBox.css'; -@import './Popover.css'; -@import './Button.css'; -@import './Form.css'; -@import "./theme.css"; - -.react-aria-Select { - color: var(--text-color); - - .react-aria-Button { - box-shadow: 0 1px 2px rgba(0 0 0 / 0.1); - border-radius: 6px; - font-size: 1.072rem; - padding: 0.286rem 0.286rem 0.286rem 0.571rem; - display: flex; - align-items: center; - max-width: 250px; - - &[data-focus-visible] { - outline: 2px solid var(--focus-ring-color); - outline-offset: -1px; - } - } - - .react-aria-SelectValue { - &[data-placeholder] { - font-style: italic; - color: var(--text-color-placeholder); - } - } - - span[aria-hidden] { - width: 1.5rem; - line-height: 1.375rem; - margin-left: 1rem; - padding: 1px; - background: var(--highlight-background); - color: var(--highlight-foreground); - forced-color-adjust: none; - border-radius: 4px; - font-size: 0.857rem; - } -} - -.react-aria-Popover[data-trigger=Select] { - min-width: var(--trigger-width); - - .react-aria-ListBox { - display: block; - width: unset; - max-height: inherit; - min-height: unset; - border: none; - - .react-aria-Header { - padding-left: 1.571rem; - } - } - - .react-aria-ListBoxItem { - padding: 0.286rem 0.571rem 0.286rem 1.571rem; - - &[data-focus-visible] { - outline: none; - } - - &[data-selected] { - font-weight: 600; - background: unset; - color: var(--text-color); - - &::before { - content: '✓'; - content: '✓' / ''; - alt: ' '; - position: absolute; - top: 4px; - left: 4px; - } - } - - &[data-focused], - &[data-pressed] { - background: var(--highlight-background); - color: var(--highlight-foreground); - } - } -} - -.react-aria-ListBoxItem[href] { - text-decoration: none; - cursor: pointer; -} - -.react-aria-Select { - .react-aria-SelectValue { - [slot=description] { - display: none; - } - } - - .react-aria-Button { - &[data-disabled] { - border-color: var(--border-color-disabled); - color: var(--text-color-disabled); - span[aria-hidden] { - background: var(--border-color-disabled); - color: var(--text-color-disabled); - } - - .react-aria-SelectValue { - &[data-placeholder] { - color: var(--text-color-disabled); - } - } - } - } -} - -@media (forced-colors: active) { - .react-aria-Select { - .react-aria-Button { - &[data-disabled] span[aria-hidden] { - background: 0 0; - } - } - } -} - -.react-aria-Select { - .react-aria-FieldError { - font-size: 12px; - color: var(--invalid-color); - } - - [slot=description] { - font-size: 12px; - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.tsx deleted file mode 100644 index 66b964613..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Select.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react' -import { - Button, - FieldError, - Label, - ListBox, - ListBoxItem, - ListBoxItemProps, - Popover, - Select as AriaSelect, - SelectProps as AriaSelectProps, - SelectValue, - Text, - ValidationResult, -} from 'react-aria-components' - -import './Select.css' - -export interface SelectProps - extends Omit, 'children'> { - label?: string - description?: string - errorMessage?: string | ((validation: ValidationResult) => string) - items?: Iterable - children: React.ReactNode | ((item: T) => React.ReactNode) -} - -export function Select({ - label, - description, - errorMessage, - children, - items, - ...props -}: SelectProps) { - return ( - - - - {description && {description}} - {errorMessage} - - {children} - - - ) -} - -export { Select as MySelect } - -export function SelectItem(props: ListBoxItemProps) { - return -} - -export { SelectItem as MyItem } diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.css deleted file mode 100644 index adf5fcbcf..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.css +++ /dev/null @@ -1,74 +0,0 @@ -@import "./theme.css"; - -.react-aria-Switch { - display: flex; - align-items: center; - gap: 0.571rem; - font-size: 1.143rem; - color: var(--text-color); - forced-color-adjust: none; - - .indicator { - width: 2rem; - height: 1.143rem; - border: 2px solid var(--border-color); - background: var(--background-color); - border-radius: 1.143rem; - transition: all 200ms; - - &:before { - content: ''; - display: block; - margin: 0.143rem; - width: 0.857rem; - height: 0.857rem; - background: var(--highlight-background); - border-radius: 16px; - transition: all 200ms; - } - } - - &[data-pressed] .indicator { - border-color: var(--border-color-pressed); - - &:before { - background: var(--highlight-background-pressed); - } - } - - &[data-selected] { - .indicator { - border-color: var(--highlight-background); - background: var(--highlight-background); - - &:before { - background: var(--field-background); - transform: translateX(100%); - } - } - - &[data-pressed] { - .indicator { - border-color: var(--highlight-background-pressed); - background: var(--highlight-background-pressed); - } - } - } - - &[data-focus-visible] .indicator { - outline: 2px solid var(--focus-ring-color); - outline-offset: 2px; - } - - &[data-disabled] { - color: var(--text-color-disabled); - - .indicator { - border-color: var(--border-color-disabled); - - &:before { - background: var(--border-color-disabled); - } - } - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.tsx deleted file mode 100644 index b1b237e12..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { - Switch as AriaSwitch, - SwitchProps as AriaSwitchProps, -} from 'react-aria-components' -import React from 'react' -import './Switch.css' - -export interface SwitchProps extends Omit { - children: React.ReactNode -} - -export function Switch({ children, ...props }: SwitchProps) { - return ( - -
- {children} - - ) -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.css deleted file mode 100644 index c18b5f8f7..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.css +++ /dev/null @@ -1,102 +0,0 @@ -@import './Button.css'; -@import './Link.css'; -@import "./theme.css"; - -.react-aria-Tabs { - display: flex; - color: var(--text-color); - - &[data-orientation=horizontal] { - flex-direction: column; - } -} - -.react-aria-TabList { - display: flex; - - &[data-orientation=horizontal] { - border-bottom: 1px solid var(--border-color); - - .react-aria-Tab { - border-bottom: 3px solid var(--border-color); - } - } -} - -.react-aria-Tab { - padding: 10px; - cursor: default; - outline: none; - position: relative; - color: var(--text-color-base); - transition: color 200ms; - --border-color: transparent; - forced-color-adjust: none; - - &[data-hovered], - &[data-focused] { - color: var(--text-color-hover); - } - - &[data-selected] { - --border-color: var(--highlight-background); - color: var(--text-color); - } - - &[data-disabled] { - color: var(--text-color-disabled); - &[data-selected] { - --border-color: var(--text-color-disabled); - } - } - - &[data-focus-visible]:after { - content: ''; - position: absolute; - inset: 4px; - border-radius: 4px; - border: 2px solid var(--focus-ring-color); - } -} - -.react-aria-TabPanel { - margin-top: 4px; - padding: 10px; - border-radius: 4px; - outline: none; - - &[data-focus-visible] { - outline: 2px solid var(--focus-ring-color); - } -} - -.react-aria-Tabs { - &[data-orientation=vertical] { - flex-direction: row; - } -} - -.react-aria-TabList { - &[data-orientation=vertical] { - flex-direction: column; - border-inline-end: 1px solid gray; - - .react-aria-Tab { - border-inline-end: 3px solid var(--border-color, transparent); - } - } -} - -.react-aria-Tab { - &[data-disabled] { - color: var(--text-color-disabled); - &[data-selected] { - --border-color: var(--border-color-disabled); - } - } -} - -.react-aria-Tab[href] { - text-decoration: none; - cursor: pointer; -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.tsx deleted file mode 100644 index aa911b9e3..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import { Tabs as RACTabs, TabsProps } from 'react-aria-components' -import './Tabs.css' - -export function Tabs(props: TabsProps) { - return -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.css deleted file mode 100644 index 1a84e2d89..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.css +++ /dev/null @@ -1,49 +0,0 @@ -@import './Button.css'; -@import "./theme.css"; - -.react-aria-TextField { - display: flex; - flex-direction: column; - width: fit-content; - color: var(--text-color); - - .react-aria-Input, - .react-aria-TextArea { - padding: 0.286rem; - margin: 0; - border: 1px solid var(--border-color); - border-radius: 6px; - background: var(--field-background); - font-size: 1.143rem; - color: var(--field-text-color); - - &[data-focused] { - outline: 2px solid var(--focus-ring-color); - outline-offset: -1px; - } - } - - .react-aria-Input, - .react-aria-TextArea { - &[data-invalid] { - border-color: var(--invalid-color); - } - } - - .react-aria-FieldError { - font-size: 12px; - color: var(--invalid-color); - } - - [slot=description] { - font-size: 12px; - } - - .react-aria-Input, - .react-aria-TextArea { - &[data-disabled] { - border-color: var(--border-color-disabled); - color: var(--text-color-disabled); - } - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.tsx deleted file mode 100644 index f2159b20a..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/TextField.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { - FieldError, - Input, - Label, - Text, - TextField as AriaTextField, - TextFieldProps as AriaTextFieldProps, -} from 'react-aria-components' - -import './TextField.css' -import React from 'react' - -export interface TextFieldProps extends AriaTextFieldProps { - label?: string - description?: string -} - -export function TextField({ label, description, ...props }: TextFieldProps) { - const [isError, setIsError] = React.useState(false) - return ( - setIsError(v.includes('error'))} - isInvalid={isError} - > - - - {description && {description}} - {isError && "Some error!"} - - ) -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/theme.css b/packages/signals/signals-integration-tests/src/tests/custom-elements/components/theme.css deleted file mode 100644 index 23dab3f8e..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/components/theme.css +++ /dev/null @@ -1,127 +0,0 @@ -/* Base styles */ -:root { - font-family: system-ui; - font-size: 14px; - line-height: 1.5; - background: var(--background-color); -} - -/* color themes for dark and light modes, generated with Leonardo. - * Light: https://leonardocolor.io/theme.html?name=Light&config=%7B%22baseScale%22%3A%22Gray%22%2C%22colorScales%22%3A%5B%7B%22name%22%3A%22Gray%22%2C%22colorKeys%22%3A%5B%22%23000000%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Purple%22%2C%22colorKeys%22%3A%5B%22%235e30eb%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Red%22%2C%22colorKeys%22%3A%5B%22%23e32400%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%5D%2C%22lightness%22%3A98%2C%22contrast%22%3A1%2C%22saturation%22%3A100%2C%22formula%22%3A%22wcag2%22%7D */ -:root { - --background-color: #f8f8f8; - --gray-50: #ffffff; - --gray-100: #d0d0d0; - --gray-200: #afafaf; - --gray-300: #8f8f8f; - --gray-400: #717171; - --gray-500: #555555; - --gray-600: #393939; - --purple-100: #d5c9fa; - --purple-200: #b8a3f6; - --purple-300: #997cf2; - --purple-400: #7a54ef; - --purple-500: #582ddc; - --purple-600: #3c1e95; - --red-100: #f7c4ba; - --red-200: #f29887; - --red-300: #eb664d; - --red-400: #de2300; - --red-500: #a81b00; - --red-600: #731200; - --highlight-hover: rgb(0 0 0 / 0.07); - --highlight-pressed: rgb(0 0 0 / 0.15); -} - -/* Dark: https://leonardocolor.io/theme.html?name=Dark&config=%7B%22baseScale%22%3A%22Gray%22%2C%22colorScales%22%3A%5B%7B%22name%22%3A%22Gray%22%2C%22colorKeys%22%3A%5B%22%23000000%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Purple%22%2C%22colorKeys%22%3A%5B%22%235e30eb%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%2C%7B%22name%22%3A%22Red%22%2C%22colorKeys%22%3A%5B%22%23e32400%22%5D%2C%22colorspace%22%3A%22RGB%22%2C%22ratios%22%3A%5B%22-1.12%22%2C%221.45%22%2C%222.05%22%2C%223.02%22%2C%224.54%22%2C%227%22%2C%2210.86%22%5D%2C%22smooth%22%3Afalse%7D%5D%2C%22lightness%22%3A11%2C%22contrast%22%3A1%2C%22saturation%22%3A100%2C%22formula%22%3A%22wcag2%22%7D */ -@media (prefers-color-scheme: dark) { - :root { - --background-color: #1d1d1d; - --gray-50: #101010; - --gray-100: #393939; - --gray-200: #4f4f4f; - --gray-300: #686868; - --gray-400: #848484; - --gray-500: #a7a7a7; - --gray-600: #cfcfcf; - --purple-100: #3c1e95; - --purple-200: #522acd; - --purple-300: #6f46ed; - --purple-400: #8e6ef1; - --purple-500: #b099f5; - --purple-600: #d5c8fa; - --red-100: #721200; - --red-200: #9c1900; - --red-300: #cc2000; - --red-400: #e95034; - --red-500: #f08c79; - --red-600: #f7c3ba; - --highlight-hover: rgb(255 255 255 / 0.1); - --highlight-pressed: rgb(255 255 255 / 0.2); - } -} - -/* Semantic colors */ -:root { - --focus-ring-color: var(--purple-400); - --text-color: var(--gray-600); - --text-color-base: var(--gray-500); - --text-color-hover: var(--gray-600); - --text-color-disabled: var(--gray-200); - --text-color-placeholder: var(--gray-400); - --link-color: var(--purple-500); - --link-color-secondary: var(--gray-500); - --link-color-pressed: var(--purple-600); - --border-color: var(--gray-300); - --border-color-hover: var(--gray-400); - --border-color-pressed: var(--gray-400); - --border-color-disabled: var(--gray-100); - --field-background: var(--gray-50); - --field-text-color: var(--gray-600); - --overlay-background: var(--gray-50); - --button-background: var(--gray-50); - --button-background-pressed: var(--background-color); - /* these colors are the same between light and dark themes - * to ensure contrast with the foreground color */ - --highlight-background: #6f46ed; /* purple-300 from dark theme, 3.03:1 against background-color */ - --highlight-background-pressed: #522acd; /* purple-200 from dark theme */ - --highlight-background-invalid: #cc2000; /* red-300 from dark theme */ - --highlight-foreground: white; /* 5.56:1 against highlight-background */ - --highlight-foreground-pressed: #ddd; - --highlight-overlay: rgb(from #6f46ed r g b / 15%); - --invalid-color: var(--red-400); - --invalid-color-pressed: var(--red-500); -} - -/* Windows high contrast mode overrides */ -@media (forced-colors: active) { - :root { - --background-color: Canvas; - --focus-ring-color: Highlight; - --text-color: ButtonText; - --text-color-base: ButtonText; - --text-color-hover: ButtonText; - --text-color-disabled: GrayText; - --text-color-placeholder: ButtonText; - --link-color: LinkText; - --link-color-secondary: LinkText; - --link-color-pressed: LinkText; - --border-color: ButtonBorder; - --border-color-hover: ButtonBorder; - --border-color-pressed: ButtonBorder; - --border-color-disabled: GrayText; - --field-background: Field; - --field-text-color: FieldText; - --overlay-background: Canvas; - --button-background: ButtonFace; - --button-background-pressed: ButtonFace; - --highlight-background: Highlight; - --highlight-background-pressed: Highlight; - --highlight-background-invalid: LinkText; - --highlight-foreground: HighlightText; - --highlight-foreground-pressed: HighlightText; - --invalid-color: LinkText; - --invalid-color-pressed: LinkText; - } -} - diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-select.test.ts b/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-select.test.ts deleted file mode 100644 index 03b2ed2d7..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-select.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { test, expect } from '@playwright/test' -import { waitForCondition } from '../../helpers/playwright-utils' -import { IndexPage } from './index-page' -import type { SegmentEvent } from '@segment/analytics-next' - -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` - -test('Collecting signals whenever a user selects an item', async ({ page }) => { - const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { - disableSignalsRedaction: true, - enableSignalsIngestion: true, - }) - - const filterClick = (e: SegmentEvent): boolean => { - return ( - e.properties!.data.eventType === 'click' && - e.properties!.data.target.textContent?.includes('Mint') - ) - } - - const waitForInteraction = waitForCondition( - () => { - const events = indexPage.signalsAPI.getEvents('interaction') - return events.some(filterClick) - }, - { errorMessage: 'No interaction signals found' } - ) - await page.click('#select button') - await page.getByRole('option', { name: 'Mint' }).click() - - await waitForInteraction - const signals = indexPage.signalsAPI - .getEvents('interaction') - .filter(filterClick) - - expect(signals).toHaveLength(1) -}) diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-textfield.test.ts b/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-textfield.test.ts deleted file mode 100644 index b7199fbeb..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/custom-textfield.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { test, expect } from '@playwright/test' -import { waitForCondition } from '../../helpers/playwright-utils' -import { IndexPage } from './index-page' - -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` - -test('Collecting signals whenever a user enters text input and focuses out', async ({ - page, -}) => { - const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { - disableSignalsRedaction: true, - enableSignalsIngestion: true, - }) - const fillAndConfirm = async (selector: string, text: string) => { - await page.getByTestId(selector).fill(text) - await page.getByTestId(selector).press('Enter') - } - await Promise.all([ - fillAndConfirm('aria-text-field', 'John Doe'), - waitForCondition( - () => indexPage.signalsAPI.getEvents('interaction').length > 0, - { errorMessage: 'No interaction signals found' } - ), - ]) - const interactionSignals = indexPage.signalsAPI.getEvents('interaction') - - const data = expect.objectContaining({ - eventType: 'change', - listener: 'mutation', - change: { - value: 'John Doe', - }, - target: expect.objectContaining({ - attributes: expect.objectContaining({ - type: 'text', - value: 'John Doe', - }), - tagName: 'INPUT', - value: 'John Doe', - }), - }) - expect(interactionSignals[0].properties!.data).toMatchObject(data) -}) diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/index-page.ts b/packages/signals/signals-integration-tests/src/tests/custom-elements/index-page.ts deleted file mode 100644 index 57f96df6c..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/index-page.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BasePage } from '../../helpers/base-page-object' - -export class IndexPage extends BasePage { - constructor() { - super(`/custom-elements/index.html`) - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/index.bundle.tsx b/packages/signals/signals-integration-tests/src/tests/custom-elements/index.bundle.tsx deleted file mode 100644 index 53deff5e2..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/index.bundle.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import { createRoot } from 'react-dom/client' -import { AnalyticsBrowser } from '@segment/analytics-next' -import { SignalsPlugin } from '@segment/analytics-signals' -import { App } from './components/App' - -window.SignalsPlugin = SignalsPlugin -window.analytics = new AnalyticsBrowser() - -const container = document.getElementById('root') -const root = createRoot(container!) -root.render() diff --git a/packages/signals/signals-integration-tests/src/tests/custom-elements/index.html b/packages/signals/signals-integration-tests/src/tests/custom-elements/index.html deleted file mode 100644 index b526ff924..000000000 --- a/packages/signals/signals-integration-tests/src/tests/custom-elements/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - > - - - -
- - - - diff --git a/packages/signals/signals-integration-tests/src/tests/performance/index-page.ts b/packages/signals/signals-integration-tests/src/tests/performance/index-page.ts deleted file mode 100644 index 6b8df50ed..000000000 --- a/packages/signals/signals-integration-tests/src/tests/performance/index-page.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BasePage } from '../../helpers/base-page-object' - -export class IndexPage extends BasePage { - constructor() { - super(`/performance/index.html`) - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/performance/index.html b/packages/signals/signals-integration-tests/src/tests/performance/index.html deleted file mode 100644 index 68623ba30..000000000 --- a/packages/signals/signals-integration-tests/src/tests/performance/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - -
- - - diff --git a/packages/signals/signals-integration-tests/src/tests/performance/memory-leak.test.ts b/packages/signals/signals-integration-tests/src/tests/performance/memory-leak.test.ts deleted file mode 100644 index 2d1a7e891..000000000 --- a/packages/signals/signals-integration-tests/src/tests/performance/memory-leak.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { test, expect, Page } from '@playwright/test' -import { IndexPage } from './index-page' -import { sleep } from '@segment/analytics-core' - -declare global { - interface Window { - gc: () => void // chrome specific - } - interface Performance { - memory: { - usedJSHeapSize: number - totalJSHeapSize: number - } - } -} - -const basicEdgeFn = ` - // this is a process signal function - globalThis.processSignal = (signal) => { - if (signal.type === 'interaction') { - const eventName = signal.data.eventType + ' ' + '[' + signal.type + ']' - analytics.track(eventName, signal.data) - } - }` - -const checkForMemoryLeak = async (page: Page) => { - const ALLOWED_GROWTH = 1.1 - const getMemoryUsage = (): Promise => - page.evaluate(() => { - return performance.memory.usedJSHeapSize - }) - - const firstMemory = await getMemoryUsage() - - // add nodes - await page.evaluate(() => { - const target = document.getElementById('test-container')! - const NODE_COUNT = 2000 - for (let i = 0; i < NODE_COUNT; i++) { - const newNode = document.createElement('input') - newNode.type = 'text' - newNode.value = Math.random().toString() - target.appendChild(newNode) - } - }) - - // remove all the nodes - await page.evaluate(() => { - const target = document.getElementById('test-container')! - while (target.firstChild) { - target.removeChild(target.firstChild) - } - }) - const inputNodeLength = await page.evaluate( - () => document.querySelectorAll('input').length - ) - expect(inputNodeLength).toBe(0) - - await page.evaluate(() => { - // force run garbage collection: --js-flags="--expose-gc" is required - window.gc() - }) - - await sleep(500) // may not be needed, but just in case. - - const lastMemory = await getMemoryUsage() // Allow some fluctuation, but fail if there's a significant memory increase - const report = `initial: ${firstMemory}, final: ${lastMemory}, allowed growth: ${ALLOWED_GROWTH}` - if (lastMemory > firstMemory * ALLOWED_GROWTH) { - throw new Error(`Memory leak detected! ${report}`) - } else { - console.log('Memory leak test passed!', `initial: ${report}`) - } -} - -test('memory leak test scaffold works', async ({ page }) => { - const htmlContent = ` - - - - Test Page - - -
- - - ` - await page.setContent(htmlContent) - await page.waitForLoadState('networkidle') - await checkForMemoryLeak(page) -}) - -test('memory leak', async ({ page }) => { - const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn) - await indexPage.waitForSignalsApiFlush() - await page.waitForLoadState('networkidle') - await checkForMemoryLeak(page) -}) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts deleted file mode 100644 index 188cdaf28..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { test, expect } from '@playwright/test' -import { IndexPage } from './index-page' -import fs from 'fs' -import path from 'path' -import { SegmentEvent } from '@segment/analytics-next' -/** - * This test ensures that - */ -const indexPage = new IndexPage() - -const normalizeSnapshotEvent = (el: SegmentEvent) => { - return { - type: el.type, - event: el.event, - userId: el.userId, - groupId: el.groupId, - anonymousId: expect.any(String), - integrations: el.integrations, - properties: el.properties, - context: { - page: el.context?.page, - }, - } -} - -const snapshot = ( - JSON.parse( - fs.readFileSync( - path.join(__dirname, 'snapshots/all-segment-events-snapshot.json'), - { - encoding: 'utf-8', - } - ) - ) as SegmentEvent[] -).map(normalizeSnapshotEvent) - -test('Segment events', async ({ page }) => { - const basicEdgeFn = ` - // this is a process signal function - globalThis.processSignal = (signal) => { - if (signal.type === 'interaction' && signal.data.eventType === 'click') { - analytics.identify('john', { found: true }) - analytics.group('foo', { hello: 'world' }) - analytics.alias('john', 'johnsmith') - analytics.track('a track call', {foo: 'bar'}) - analytics.page('Retail Page', 'Home', { url: 'http://my-home.com', title: 'Some Title' }); - } - }` - - await indexPage.load(page, basicEdgeFn) - const flush = Promise.all([ - indexPage.waitForSignalsApiFlush(), - indexPage.waitForTrackingApiFlush(), - ]) - await indexPage.clickButton() - await flush - - const trackingApiReqs = indexPage.trackingAPI - .getEvents() - .map(normalizeSnapshotEvent) - expect(trackingApiReqs).toEqual(snapshot) -}) - -test('Should dispatch events from signals that occurred before analytics was instantiated', async ({ - page, -}) => { - const edgeFn = ` - globalThis.processSignal = (signal) => { - if (signal.type === 'navigation') { - analytics.page('dispatched from signals - navigation') - } - if (signal.type === 'userDefined') { - analytics.track('dispatched from signals - userDefined') - } - }` - - await indexPage.load(page, edgeFn) - const flush = Promise.all([ - indexPage.waitForSignalsApiFlush(), - indexPage.waitForTrackingApiFlush(), - ]) - - // add a user defined signal before analytics is instantiated - void indexPage.addUserDefinedSignal() - await flush - - const trackingApiReqs = indexPage.trackingAPI.getEvents() - expect(trackingApiReqs).toHaveLength(2) - - const pageEvents = trackingApiReqs.find((el) => el.type === 'page')! - expect(pageEvents).toBeTruthy() - expect(pageEvents.name).toEqual('dispatched from signals - navigation') - - const userDefinedEvents = trackingApiReqs.find((el) => el.type === 'track')! - expect(userDefinedEvents).toBeTruthy() - expect(userDefinedEvents.event).toEqual( - 'dispatched from signals - userDefined' - ) -}) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts deleted file mode 100644 index 45cb006ec..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { test, expect } from '@playwright/test' -import { commonSignalData } from '../../helpers/fixtures' -import { IndexPage } from './index-page' - -const basicEdgeFn = ` - // this is a process signal function - globalThis.processSignal = (signal) => { - if (signal.type === '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('network signals fetch', async () => { - /** - * Make a fetch call, see if it gets sent to the signals endpoint - */ - await indexPage.network.mockTestRoute() - await indexPage.network.makeFetchCall() - await indexPage.waitForSignalsApiFlush() - const networkEvents = indexPage.signalsAPI.getEvents('network') - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - expect(requests).toHaveLength(1) - expect(requests[0].properties!.data.body).toEqual({ foo: 'bar' }) - - const responses = networkEvents.filter( - (el) => el.properties!.data.action === 'response' - ) - expect(responses).toHaveLength(1) - expect(responses[0].properties!.data.body).toEqual({ someResponse: 'yep' }) -}) - -test('network signals xhr', async () => { - /** - * Make a xhr call, see if it gets sent to the signals endpoint - */ - await indexPage.network.mockTestRoute() - await indexPage.network.makeXHRCall() - await indexPage.waitForSignalsApiFlush() - const networkEvents = indexPage.signalsAPI.getEvents('network') - expect(networkEvents).toHaveLength(2) - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - expect(requests).toHaveLength(1) - expect(requests[0].properties!.data.body).toEqual({ foo: 'bar' }) - - const responses = networkEvents.filter( - (el) => el.properties!.data.action === 'response' - ) - expect(responses).toHaveLength(1) - expect(responses[0].properties!.data.body).toEqual({ someResponse: 'yep' }) - expect(responses[0].properties!.data.page).toEqual(commonSignalData.page) -}) - -test('instrumentation signals', async () => { - /** - * Make an analytics.page() call, see if it gets sent to the signals endpoint - */ - await Promise.all([ - indexPage.makeAnalyticsPageCall(), - indexPage.waitForSignalsApiFlush(), - ]) - - const isoDateRegEx = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ - const instrumentationEvents = - indexPage.signalsAPI.getEvents('instrumentation') - expect(instrumentationEvents).toHaveLength(1) - const ev = instrumentationEvents[0] - expect(ev.event).toBe('Segment Signal Generated') - expect(ev.type).toBe('track') - const rawEvent = ev.properties!.data.rawEvent - expect(rawEvent).toMatchObject({ - type: 'page', - anonymousId: expect.any(String), - timestamp: expect.stringMatching(isoDateRegEx), - }) -}) - -test('interaction signals', async () => { - /** - * Make a button click, see if 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.waitForSignalsApiFlush(), - indexPage.waitForTrackingApiFlush(), - ]) - - const interactionSignals = indexPage.signalsAPI.getEvents('interaction') - expect(interactionSignals).toHaveLength(1) - const data = { - eventType: 'click', - target: { - attributes: { - id: 'some-button', - }, - classList: [], - id: 'some-button', - labels: [], - name: '', - nodeName: 'BUTTON', - tagName: 'BUTTON', - title: '', - type: 'submit', - value: '', - }, - page: commonSignalData.page, - } - - expect(interactionSignals[0]).toMatchObject({ - event: 'Segment Signal Generated', - type: 'track', - properties: { - type: 'interaction', - data, - }, - }) - - const analyticsReqJSON = indexPage.trackingAPI.lastEvent() - expect(analyticsReqJSON).toMatchObject({ - writeKey: '', - event: 'click [interaction]', - properties: data, - context: { - __eventOrigin: { - type: 'Signal', - }, - }, - }) -}) - -test('navigation signals', async ({ page }) => { - /** - * Load a page and then click, see if it: - * - creates a navigation signal that sends to the signals endpoint - * Click a link, see if it - * - creates a navigation signal that sends to the signals endpoint - */ - { - // on page load, a navigation signal should be sent - await indexPage.waitForSignalsApiFlush() - expect(indexPage.signalsAPI.getEvents()).toHaveLength(1) - const ev = indexPage.signalsAPI.lastEvent('navigation') - expect(ev.properties).toMatchObject({ - type: 'navigation', - data: { - currentUrl: indexPage.url, - path: expect.any(String), - hash: '', - search: '', - title: '', - }, - }) - } - - // navigate to a new hash - { - const flush = indexPage.waitForSignalsApiFlush() - await page.evaluate(() => { - window.location.hash = '#foo' - }) - await flush - expect(indexPage.signalsAPI.getEvents()).toHaveLength(2) - const ev = indexPage.signalsAPI.lastEvent('navigation') - expect(ev.properties).toMatchObject({ - index: expect.any(Number), - type: 'navigation', - data: { - currentUrl: indexPage.url + '#foo', - previousUrl: indexPage.url, - path: expect.any(String), - hash: '#foo', - search: '', - title: '', - page: expect.any(Object), - }, - }) - } -}) 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 deleted file mode 100644 index 864dec3b1..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/button-click-complex.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { test, expect } from '@playwright/test' -import { IndexPage } from './index-page' - -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` -let indexPage: IndexPage -test.beforeEach(async ({ page }) => { - indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn) -}) - -const data = { - eventType: 'click', - target: { - attributes: { - id: 'complex-button', - }, - classList: [], - id: 'complex-button', - labels: [], - name: '', - nodeName: 'BUTTON', - tagName: 'BUTTON', - title: '', - type: 'submit', - innerText: 'Other Example Button with Nested Text', - textContent: 'Other Example Button with Nested Text', - value: '', - }, -} - -test('clicking a button with nested content', async () => { - /** - * Click a button with nested text, ensure that that correct text shows up - */ - await Promise.all([ - indexPage.clickComplexButton(), - indexPage.waitForSignalsApiFlush(), - ]) - - const interactionSignals = indexPage.signalsAPI.getEvents('interaction') - expect(interactionSignals).toHaveLength(1) - - expect(interactionSignals[0]).toMatchObject({ - event: 'Segment Signal Generated', - type: 'track', - properties: { - type: 'interaction', - data, - }, - }) -}) - -test('clicking the h1 tag inside a button', async () => { - /** - * Click the nested text, ensure that that correct text shows up - */ - await Promise.all([ - indexPage.clickInsideComplexButton(), - indexPage.waitForSignalsApiFlush(), - ]) - - const interactionSignals = indexPage.signalsAPI.getEvents('interaction') - expect(interactionSignals).toHaveLength(1) - expect(interactionSignals[0]).toMatchObject({ - event: 'Segment Signal Generated', - type: 'track', - properties: { - type: 'interaction', - 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 deleted file mode 100644 index df297f8b6..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { test, expect } from '@playwright/test' -import { waitForCondition } from '../../helpers/playwright-utils' -import { IndexPage } from './index-page' - -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` - -test('Collecting signals whenever a user enters text input', async ({ - page, -}) => { - /** - * Input some text into the input field, see if the signal is emitted correctly - */ - const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { - disableSignalsRedaction: true, - enableSignalsIngestion: true, - }) - - await Promise.all([ - indexPage.fillNameInput('John Doe'), - indexPage.waitForSignalsApiFlush(), - ]) - - await waitForCondition( - () => indexPage.signalsAPI.getEvents('interaction').length > 0, - { errorMessage: 'No interaction signals found' } - ) - const interactionSignals = indexPage.signalsAPI.getEvents('interaction') - - const data = { - eventType: 'change', - target: expect.objectContaining({ - attributes: expect.objectContaining({ - type: 'text', - id: 'name', - name: 'name', - }), - classList: [], - id: 'name', - labels: [ - { - textContent: 'Name:', - id: '', - attributes: { - for: 'name', - }, - }, - ], - name: 'name', - nodeName: 'INPUT', - tagName: 'INPUT', - value: 'John Doe', - }), - } - expect(interactionSignals[0].properties!.data).toMatchObject(data) -}) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index-page.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index-page.ts deleted file mode 100644 index a1a7f3220..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index-page.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { BasePage } from '../../helpers/base-page-object' -import { promiseTimeout } from '@internal/test-helpers' -import { fillAndBlur } from '../../helpers/playwright-utils' - -export class IndexPage extends BasePage { - constructor() { - super(`/signals-vanilla/index.html`) - } - - async makeAnalyticsPageCall(): Promise { - const p = this.page.evaluate(() => { - void window.analytics.page() - return new Promise((resolve) => window.analytics.on('page', resolve)) - }) - return promiseTimeout(p, 2000, 'analytics.on("page") did not resolve') - } - - async makeAnalyticsTrackCall(): Promise { - const p = this.page.evaluate(() => { - void window.analytics.track('some event') - return new Promise((resolve) => window.analytics.on('track', resolve)) - }) - return promiseTimeout(p, 2000, 'analytics.on("track") did not resolve') - } - - addUserDefinedSignal(data?: Record) { - return this.page.evaluate( - (args) => { - window.signalsPlugin.addSignal({ - foo: 'bar', - ...args.data, - }) - }, - { data } - ) - } - - async clickButton() { - return this.page.click('#some-button') - } - - async clickComplexButton() { - return this.page.click('#complex-button') - } - - async clickInsideComplexButton() { - return this.page.click('#complex-button h1') - } - - async fillNameInput(text: string) { - return await fillAndBlur(this.page, '#name', text) - } -} diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.bundle.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.bundle.ts deleted file mode 100644 index afe980c40..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.bundle.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { AnalyticsBrowser } from '@segment/analytics-next' -import { SignalsPlugin } from '@segment/analytics-signals' - -/** - * Not calling analytics.load() or instantiating Signals Plugin here, as all this configuration happens in the page object. - */ -window.SignalsPlugin = SignalsPlugin -window.analytics = new AnalyticsBrowser() diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.html b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.html deleted file mode 100644 index 26c931712..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - -
- -

- -

-
-

- -
- - diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/middleware.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/middleware.test.ts deleted file mode 100644 index 244a787d7..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/middleware.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { test, expect } from '@playwright/test' -import { IndexPage } from './index-page' - -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` - -let indexPage: IndexPage - -test('middleware', async ({ page }) => { - indexPage = await new IndexPage().load( - page, - basicEdgeFn, - {}, - { - skipSignalsPluginInit: true, - } - ) - - await page.evaluate( - ({ settings }) => { - window.signalsPlugin = new window.SignalsPlugin({ - middleware: [ - { - load() { - return undefined - }, - process: function (signal) { - // @ts-ignore - signal.data['middleware'] = 'test' - return signal - }, - }, - ], - ...settings, - }) - window.analytics.load({ - writeKey: '', - plugins: [window.signalsPlugin], - }) - }, - { - settings: { - ...indexPage.defaultSignalsPluginTestSettings, - flushAt: 1, - }, - } - ) - - /** - * Make an analytics.page() call, see that the middleware can modify the event - */ - await Promise.all([ - indexPage.makeAnalyticsPageCall(), - indexPage.waitForSignalsApiFlush(), - ]) - - const instrumentationEvents = - indexPage.signalsAPI.getEvents('instrumentation') - expect(instrumentationEvents).toHaveLength(1) - const ev = instrumentationEvents[0] - expect(ev.properties!.data['middleware']).toEqual('test') -}) 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 deleted file mode 100644 index 6ce8686a3..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-allow-list.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { test, expect } from '@playwright/test' -import { IndexPage } from './index-page' - -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` - -test('network signals allow and disallow list', async ({ page }) => { - const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { - networkSignalsAllowList: ['allowed-api.com'], - networkSignalsDisallowList: ['https://disallowed-api.com/api/foo'], - }) - - // test that the allowed signals were emitted + sent - const ALLOWED_URL = 'https://allowed-api.com/api/bar' - const emittedNetworkSignalsAllowed = indexPage.waitForSignalsEmit( - (el) => el.type === 'network' - ) - await indexPage.network.mockTestRoute(ALLOWED_URL) - await indexPage.network.makeFetchCall(ALLOWED_URL) - await emittedNetworkSignalsAllowed - - await indexPage.waitForSignalsApiFlush() - const networkEvents = indexPage.signalsAPI.getEvents('network') - const allowedRequestsAndResponses = networkEvents.filter( - (el) => el.properties!.data.url === ALLOWED_URL - ) - expect(allowedRequestsAndResponses).toHaveLength(2) - const [request, response] = allowedRequestsAndResponses - expect(request.properties!.data.body).toEqual({ - foo: 'bar', - }) - expect(response.properties!.data.body).toEqual({ - someResponse: 'yep', - }) - - // test the disallowed signals were not emitted (using the emitter to test this) - const DISALLOWED_URL = 'https://disallowed-api.com/api/foo' - const emittedNetworkSignalsDisallowed = indexPage.waitForSignalsEmit( - (el) => el.type === 'network', - { - failOnEmit: true, - } - ) - await indexPage.network.mockTestRoute(DISALLOWED_URL) - await indexPage.network.makeFetchCall(DISALLOWED_URL) - await emittedNetworkSignalsDisallowed -}) 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 deleted file mode 100644 index 20471c440..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-fetch.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { test, expect } from '@playwright/test' -import { commonSignalData } from '../../helpers/fixtures' -import { IndexPage } from './index-page' - -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` - -const NON_EMPTY_STRING = expect.stringMatching(/.+/) - -test.describe('network signals - fetch', () => { - let indexPage: IndexPage - - test.beforeEach(async ({ page }) => { - indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn) - }) - - test('should emit non-json requests but not include the data object', async () => { - await indexPage.network.mockTestRoute('http://localhost/upload', { - body: JSON.stringify({ foo: 'test' }), - }) - - await indexPage.network.makeFileUploadRequest('http://localhost/upload') - - await indexPage.waitForSignalsApiFlush() - - const networkEvents = indexPage.signalsAPI.getEvents('network') - - // Check the request - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - - expect(requests[0].properties!.data).toMatchObject({ - action: 'request', - contentType: 'multipart/form-data', - method: 'POST', - url: 'http://localhost/upload', - ...commonSignalData, - }) - }) - - test('should try to parse the body of any requests with string in the body', async () => { - await indexPage.network.mockTestRoute('http://localhost/test', { - body: JSON.stringify({ foo: 'test' }), - contentType: 'application/json', - }) - - await indexPage.network.makeFetchCall('http://localhost/test', { - method: 'POST', - body: JSON.stringify({ key: 'value' }), - contentType: 'text/plain', - }) - - await indexPage.waitForSignalsApiFlush() - - const networkEvents = indexPage.signalsAPI.getEvents('network') - - // Check the request - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - - expect(requests).toHaveLength(1) - expect(requests[0].properties!.data).toMatchObject({ - action: 'request', - url: 'http://localhost/test', - body: { key: 'value' }, - ...commonSignalData, - }) - }) - - test('should send the raw string if the request body cannot be parsed as json', async () => { - await indexPage.network.mockTestRoute('http://localhost/test', { - body: JSON.stringify({ foo: 'test' }), - contentType: 'application/json', - }) - - await indexPage.network.makeFetchCall('http://localhost/test', { - method: 'POST', - body: 'hello world', - contentType: 'text/plain', - }) - - await indexPage.waitForSignalsApiFlush() - - const networkEvents = indexPage.signalsAPI.getEvents('network') - - // Check the request - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - - expect(requests).toHaveLength(1) - expect(requests[0].properties!.data).toMatchObject({ - action: 'request', - url: 'http://localhost/test', - body: 'hello world', - ...commonSignalData, - }) - }) - - test('can make a basic json request / not break regular fetch calls', async () => { - await indexPage.network.mockTestRoute('http://localhost/test', { - body: JSON.stringify({ foo: 'test' }), - }) - - const resBody = await indexPage.network.makeFetchCall( - 'http://localhost/test', - { - method: 'POST', - body: JSON.stringify({ key: 'value' }), - contentType: 'application/json', - ...commonSignalData, - } - ) - - expect(resBody).toEqual({ foo: 'test' }) - - await indexPage.waitForSignalsApiFlush() - - const networkEvents = indexPage.signalsAPI.getEvents('network') - - // Check the request - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - expect(requests).toHaveLength(1) - const requestData = requests[0].properties!.data - expect(requestData).toMatchObject({ - action: 'request', - contentType: 'application/json', - url: 'http://localhost/test', - method: 'POST', - body: { key: 'value' }, - requestId: NON_EMPTY_STRING, - ...commonSignalData, - }) - const requestId = requestData.requestId - - const responses = networkEvents.filter( - (el) => el.properties!.data.action === 'response' - ) - expect(responses).toHaveLength(1) - const responseData = responses[0].properties!.data - expect(responseData).toMatchObject({ - action: 'response', - contentType: 'application/json', - url: 'http://localhost/test', - body: { foo: 'test' }, - status: 200, - ok: true, - requestId: NON_EMPTY_STRING, - ...commonSignalData, - }) - expect(responseData.requestId).toEqual(requestId) - }) - - test('can handle relative url paths', async () => { - await indexPage.network.mockTestRoute(`/test`, { - body: JSON.stringify({ foo: 'test' }), - contentType: 'application/json', - }) - - await indexPage.network.makeFetchCall('/test', { - method: 'POST', - body: JSON.stringify({ key: 'value' }), - contentType: 'application/json', - }) - - await indexPage.waitForSignalsApiFlush() - - const networkEvents = indexPage.signalsAPI.getEvents('network') - - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - - expect(requests).toHaveLength(1) - expect(requests[0].properties!.data).toMatchObject({ - action: 'request', - url: `${indexPage.origin()}/test`, - body: { key: 'value' }, - ...commonSignalData, - }) - - const responses = networkEvents.filter( - (el) => el.properties!.data.action === 'response' - ) - expect(responses).toHaveLength(1) - expect(responses[0].properties!.data).toMatchObject({ - action: 'response', - url: `${indexPage.origin()}/test`, - body: { foo: 'test' }, - ...commonSignalData, - }) - }) - - test.describe('errors', () => { - test('will handle a json error response', async () => { - await indexPage.network.mockTestRoute('http://localhost/test', { - status: 400, - body: JSON.stringify({ errorMsg: 'foo' }), - contentType: 'application/json', - }) - - await indexPage.network.makeFetchCall('http://localhost/test', { - method: 'POST', - body: JSON.stringify({ key: 'value' }), - contentType: 'application/json', - }) - - await indexPage.waitForSignalsApiFlush() - - const networkEvents = indexPage.signalsAPI.getEvents('network') - - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - expect(requests).toHaveLength(1) - expect(requests[0].properties!.data).toMatchObject({ - action: 'request', - url: 'http://localhost/test', - }) - - const responses = networkEvents.filter( - (el) => el.properties!.data.action === 'response' - ) - expect(responses[0].properties!.data).toMatchObject({ - action: 'response', - url: 'http://localhost/test', - body: { errorMsg: 'foo' }, - status: 400, - ok: false, - page: expect.any(Object), - }) - expect(responses).toHaveLength(1) - }) - - test('will handle a text error response', async () => { - await indexPage.network.mockTestRoute('http://localhost/test', { - status: 400, - body: 'foo', - contentType: 'text/plain', - }) - - await indexPage.network.makeFetchCall('http://localhost/test', { - method: 'POST', - body: JSON.stringify({ key: 'value' }), - contentType: 'application/json', - }) - - await indexPage.waitForSignalsApiFlush() - - const networkEvents = indexPage.signalsAPI.getEvents('network') - - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - expect(requests).toHaveLength(1) - expect(requests[0].properties!.data).toMatchObject({ - action: 'request', - url: 'http://localhost/test', - }) - - const responses = networkEvents.filter( - (el) => el.properties!.data.action === 'response' - ) - expect(responses[0].properties!.data).toMatchObject({ - action: 'response', - url: 'http://localhost/test', - body: 'foo', - status: 400, - ok: false, - ...commonSignalData, - }) - 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 deleted file mode 100644 index c69fc7e18..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-xhr.test.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { test, expect } from '@playwright/test' -import { IndexPage } from './index-page' - -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` - -test.describe('network signals - XHR', () => { - let indexPage: IndexPage - - test.beforeEach(async ({ page }) => { - indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn) - }) - - test('basic json request / not break XHR', async () => { - await indexPage.network.mockTestRoute('http://localhost/test', { - body: JSON.stringify({ foo: 'test' }), - }) - - const data = await indexPage.network.makeXHRCall('http://localhost/test', { - method: 'POST', - body: JSON.stringify({ key: 'value' }), - responseType: 'json', - contentType: 'application/json', - }) - - expect(data).toEqual({ foo: 'test' }) - - const networkEvents = await indexPage.signalsAPI.waitForEvents(2, 'network') - - // Check the request - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - expect(requests).toHaveLength(1) - expect(requests[0].properties!.data).toMatchObject({ - action: 'request', - url: 'http://localhost/test', - body: { key: 'value' }, - }) - - // Check the response - const responses = networkEvents.filter( - (el) => el.properties!.data.action === 'response' - ) - expect(responses).toHaveLength(1) - expect(responses[0].properties!.data).toMatchObject({ - action: 'response', - url: 'http://localhost/test', - body: { foo: 'test' }, - }) - }) - - test('handles relative URL paths', async () => { - await indexPage.network.mockTestRoute(`/test`, { - body: JSON.stringify({ foo: 'test' }), - contentType: 'application/json', - }) - - await indexPage.network.makeXHRCall('/test', { - method: 'POST', - body: JSON.stringify({ key: 'value' }), - responseType: 'json', - contentType: 'application/json', - }) - - // Wait for the signals to be flushed - const networkEvents = await indexPage.signalsAPI.waitForEvents(2, 'network') - - // Check the request - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - - expect(requests).toHaveLength(1) - expect(requests[0].properties!.data).toMatchObject({ - action: 'request', - url: `${indexPage.origin()}/test`, - body: { key: 'value' }, - }) - - // Check the response - const responses = networkEvents.filter( - (el) => el.properties!.data.action === 'response' - ) - expect(responses).toHaveLength(1) - expect(responses[0].properties!.data).toMatchObject({ - action: 'response', - url: `${indexPage.origin()}/test`, - body: { foo: 'test' }, - }) - }) - - test('should emit request content type, even if not json', async () => { - await indexPage.network.mockTestRoute('http://localhost/test', { - body: JSON.stringify({ foo: 'test' }), - contentType: 'application/json', - }) - - await indexPage.network.makeXHRCall('http://localhost/test', { - method: 'POST', - body: 'hello world', - responseType: 'json', - contentType: 'text/plain', - }) - - // Wait for the signals to be flushed - - const networkEvents = await indexPage.signalsAPI.waitForEvents(1, 'network') - - // ensure request - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - expect(requests).toHaveLength(1) - expect(requests[0].properties!.data).toMatchObject({ - action: 'request', - url: 'http://localhost/test', - body: 'hello world', - }) - - // Check the response - const responses = networkEvents.filter( - (el) => el.properties!.data.action === 'response' - ) - expect(responses).toHaveLength(1) - expect(responses[0].properties!.data).toMatchObject({ - action: 'response', - url: 'http://localhost/test', - body: { foo: 'test' }, - }) - }) - - test('should parse response if responseType is set to json but response header does not contain application/json', async () => { - await indexPage.network.mockTestRoute('http://localhost/test', { - body: '{"hello": "world"}', - }) - - await indexPage.network.makeXHRCall('http://localhost/test', { - responseType: 'json', - method: 'GET', - }) - - // Wait for the signals to be flushed - await indexPage.signalsAPI.waitForEvents(1, 'network') - - // Retrieve the batch of events from the signals request - const networkEvents = indexPage.signalsAPI.getEvents('network') - - // Check the response - const responses = networkEvents.filter( - (el) => el.properties!.data.action === 'response' - ) - expect(responses).toHaveLength(1) - expect(responses[0].properties!.data).toMatchObject({ - action: 'response', - url: 'http://localhost/test', - body: { hello: 'world' }, - }) - }) - - test('can handle multiple requests with variable latency', async () => { - const req1URL = 'http://localhost/test/1' - const req2URL = 'http://localhost/test/2' - - await Promise.all([ - indexPage.network.mockTestRoute(req1URL, { - body: JSON.stringify({ res1: 'test' }), - }), - indexPage.network.mockTestRoute(req2URL, { - body: JSON.stringify({ res2: 'test' }), - }), - ]) - - await Promise.all([ - indexPage.network.makeXHRCall(req1URL, { - method: 'POST', - body: JSON.stringify({ req1: 'value' }), - contentType: 'application/json', - responseType: 'json', - responseLatency: 300, - }), - indexPage.network.makeXHRCall(req2URL, { - method: 'POST', - body: JSON.stringify({ req2: 'value' }), - responseType: 'json', - contentType: 'application/json', - responseLatency: 0, - }), - ]) - - // Wait for the signals to be flushed - const networkEvents = await indexPage.signalsAPI.waitForEvents(2, 'network') - - // Check the request - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - expect(requests).toHaveLength(2) - const request1 = requests.find((u) => u.properties!.data.url === req1URL)! - expect(request1).toBeDefined() - expect(request1.properties!.data).toMatchObject({ - action: 'request', - url: req1URL, - body: { req1: 'value' }, - }) - - const request2 = requests.find((u) => u.properties!.data.url === req2URL)! - expect(request2).toBeDefined() - expect(request2.properties!.data).toMatchObject({ - action: 'request', - url: req2URL, - body: { req2: 'value' }, - }) - }) - - test.describe('errors', () => { - test('will handle a json error response', async () => { - await indexPage.network.mockTestRoute('http://localhost/test', { - status: 400, - body: JSON.stringify({ errorMsg: 'foo' }), - contentType: 'application/json', - }) - - await indexPage.network.makeXHRCall('http://localhost/test', { - method: 'POST', - body: JSON.stringify({ key: 'value' }), - responseType: 'json', // if responseType is JSON and the API returns a non-JSON response, the response will be an empty object - contentType: 'application/json', - }) - - await indexPage.signalsAPI.waitForEvents(2, 'network') - - const networkEvents = indexPage.signalsAPI.getEvents('network') - - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - expect(requests).toHaveLength(1) - expect(requests[0].properties!.data).toMatchObject({ - action: 'request', - url: 'http://localhost/test', - }) - - const responses = networkEvents.filter( - (el) => el.properties!.data.action === 'response' - ) - expect(responses[0].properties!.data).toMatchObject({ - action: 'response', - url: 'http://localhost/test', - body: { errorMsg: 'foo' }, - }) - expect(responses).toHaveLength(1) - }) - - test('will handle a text error response', async () => { - await indexPage.network.mockTestRoute('http://localhost/test', { - status: 400, - body: 'foo', - contentType: 'text/plain', - }) - - await indexPage.network.makeXHRCall('http://localhost/test', { - method: 'POST', - body: JSON.stringify({ key: 'value' }), - responseType: 'text', // if responseType is JSON and the API returns a non-JSON response, the response will be an empty object - contentType: 'application/json', - }) - - await indexPage.signalsAPI.waitForEvents(2, 'network') - - const networkEvents = indexPage.signalsAPI.getEvents('network') - - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - expect(requests).toHaveLength(1) - expect(requests[0].properties!.data).toMatchObject({ - action: 'request', - url: 'http://localhost/test', - }) - - const responses = networkEvents.filter( - (el) => el.properties!.data.action === 'response' - ) - expect(responses[0].properties!.data).toMatchObject({ - action: 'response', - url: 'http://localhost/test', - body: 'foo', - }) - expect(responses).toHaveLength(1) - }) - - test('will handle a json request and a text error response', async () => { - /** - * if the expected responseType is set to JSON and the error response is not JSON, the response will be an empty object - * This is a limitation of the XHR API, and the consumer is supposed to use responseType=text instead (but in practice, doesn't always) - **/ - await indexPage.network.mockTestRoute('http://localhost/test', { - status: 400, - body: 'I should not be parsable', - contentType: 'text/plain', - }) - - await indexPage.network.makeXHRCall('http://localhost/test', { - method: 'POST', - body: JSON.stringify({ key: 'value' }), - responseType: 'json', - contentType: 'application/json', - }) - await indexPage.signalsAPI.waitForEvents(2, 'network') - - const networkEvents = indexPage.signalsAPI.getEvents('network') - - const requests = networkEvents.filter( - (el) => el.properties!.data.action === 'request' - ) - expect(requests).toHaveLength(1) - expect(requests[0].properties!.data).toMatchObject({ - action: 'request', - url: 'http://localhost/test', - }) - - const responses = networkEvents.filter( - (el) => el.properties!.data.action === 'response' - ) - expect(responses[0].properties!.data).toMatchObject({ - action: 'response', - url: 'http://localhost/test', - body: null, - }) - expect(responses).toHaveLength(1) - }) - }) -}) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/reset.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/reset.test.ts deleted file mode 100644 index 5c42651c1..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/reset.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { test, expect } from '@playwright/test' -import { waitForCondition } from '../../helpers/playwright-utils' -import { IndexPage } from './index-page' -import { pTimeout } from '@segment/analytics-core' - -/** - * If a signal is generated, the signal buffer should be reset - * when the user clicks on the complex button. - */ -const edgeFn = `globalThis.processSignal = (signal) => { - // create a custom signal to echo out the current signal buffer - if (signal.type === 'userDefined') { - analytics.track('current signal buffer', { signalBuffer: signals.signalBuffer }) - } - - // clicking the complex button to clear the signal buffer - if (signal.type === 'interaction' && signal.data.eventType === 'click' && signal.data.target?.id === 'complex-button') { - analytics.reset() - } -}` - -test('calls analytics.reset, and resets the signalBuffer after clear', async ({ - page, -}) => { - const indexPage = await new IndexPage().loadAndWait(page, edgeFn) - - await indexPage.addUserDefinedSignal({ num: 1 }) - const resetCalled = page.evaluate(() => { - return new Promise((resolve) => { - window.analytics.on('reset', resolve) - }) - }) - - await waitForCondition(() => indexPage.trackingAPI.getEvents().length > 0, { - errorMessage: - 'No track events found, should have an event with hasSignalsInBuffer: true', - }) - const events = indexPage.trackingAPI.getEvents() - const buffer = events[0].properties!.signalBuffer - expect(buffer[0]).toMatchObject({ type: 'userDefined' }) - expect(buffer[1]).toMatchObject({ type: 'navigation' }) - - indexPage.trackingAPI.clear() - await indexPage.clickComplexButton() - await pTimeout(resetCalled, 5000) - await indexPage.addUserDefinedSignal({ num: 2 }) - await waitForCondition(() => indexPage.trackingAPI.getEvents().length > 0, { - errorMessage: - 'No track events found, should only have one event in the buffer (the current signal)', - }) - const events2 = indexPage.trackingAPI.getEvents() - const buffer2 = events2[0].properties!.signalBuffer - expect(buffer2).toHaveLength(1) - expect(buffer2[0]).toMatchObject({ type: 'userDefined' }) -}) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-find.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-find.test.ts deleted file mode 100644 index d9099f337..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-find.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { test, expect } from '@playwright/test' -import { IndexPage } from './index-page' - -const indexPage = new IndexPage() - -test('should find the most recent signal', async ({ page }) => { - const basicEdgeFn = `globalThis.processSignal = (signal) => { - if (signal.type === 'interaction' && signal.data.target.id === 'complex-button') { - const mostRecentSignal = signals.find(signal, 'userDefined') - if (mostRecentSignal.data.num === 2) { - analytics.track('correct signal found') - } - } -}` - - await indexPage.loadAndWait(page, basicEdgeFn) - const tapiFlush = indexPage.waitForTrackingApiFlush() - await indexPage.addUserDefinedSignal({ num: 1 }) - await indexPage.addUserDefinedSignal({ num: 2 }) - await indexPage.clickComplexButton() - await tapiFlush - const lastEvent = indexPage.trackingAPI.lastEvent() - expect(lastEvent.event).toEqual('correct signal found') -}) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts deleted file mode 100644 index 25f25d4b3..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { test, expect } from '@playwright/test' -import { IndexPage } from './index-page' -import { waitForCondition } from '../../helpers/playwright-utils' - -const indexPage = new IndexPage() - -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` - -test('debug ingestion disabled and sample rate 0 -> will not send the signal', async ({ - page, -}) => { - await indexPage.loadAndWait( - page, - basicEdgeFn, - { - enableSignalsIngestion: false, - flushAt: 1, - }, - { sampleRate: 0 } - ) - await indexPage.fillNameInput('John Doe') - await page.waitForTimeout(100) - expect(indexPage.signalsAPI.getEvents('interaction')).toHaveLength(0) -}) - -test('debug ingestion enabled and sample rate 0 -> will send the signal', async ({ - page, -}) => { - await indexPage.loadAndWait( - page, - basicEdgeFn, - { - enableSignalsIngestion: true, - }, - { sampleRate: 0 } - ) - - await Promise.all([ - indexPage.fillNameInput('John Doe'), - indexPage.waitForSignalsApiFlush(), - ]) - - expect(true).toBe(true) -}) - -test('debug ingestion disabled and sample rate 1 -> will send the signal', async ({ - page, -}) => { - await indexPage.loadAndWait( - page, - basicEdgeFn, - { - flushAt: 1, - enableSignalsIngestion: false, - }, - { - sampleRate: 1, - } - ) - await Promise.all([ - indexPage.fillNameInput('John Doe'), - waitForCondition( - () => indexPage.signalsAPI.getEvents('interaction').length > 0 - ), - ]) -}) 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 deleted file mode 100644 index 88e09b54d..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { test, expect } from '@playwright/test' -import { waitForCondition } from '../../helpers/playwright-utils' -import { IndexPage } from './index-page' - -const basicEdgeFn = `globalThis.processSignal = (signal) => {}` - -test('redaction enabled -> will XXX the value of text input', async ({ - page, -}) => { - const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { - disableSignalsRedaction: false, - enableSignalsIngestion: true, - }) - - await Promise.all([ - indexPage.fillNameInput('John Doe'), - indexPage.waitForSignalsApiFlush(), - ]) - - await waitForCondition( - () => indexPage.signalsAPI.getEvents('interaction').length > 0, - { errorMessage: 'No interaction signals found' } - ) - const interactionSignals = indexPage.signalsAPI.getEvents('interaction') - - const data = { - eventType: 'change', - target: expect.objectContaining({ - name: 'name', - type: 'text', - value: 'XXX', // redacted - }), - } - expect(interactionSignals[0].properties!.data).toMatchObject(data) -}) - -test('redation disabled -> will not touch the value of text input', async ({ - page, -}) => { - const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn, { - disableSignalsRedaction: true, - enableSignalsIngestion: true, - }) - await Promise.all([ - indexPage.fillNameInput('John Doe'), - indexPage.waitForSignalsApiFlush(), - ]) - - await waitForCondition( - () => indexPage.signalsAPI.getEvents('interaction').length > 0, - { errorMessage: 'No interaction signals found' } - ) - const interactionSignals = indexPage.signalsAPI.getEvents('interaction') - - const data = { - eventType: 'change', - target: expect.objectContaining({ - value: 'John Doe', // noe redacted - }), - } - expect(interactionSignals[0].properties!.data).toMatchObject(data) -}) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/snapshots/all-segment-events-snapshot.json b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/snapshots/all-segment-events-snapshot.json deleted file mode 100644 index 5b32e2d03..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/snapshots/all-segment-events-snapshot.json +++ /dev/null @@ -1,220 +0,0 @@ -[ - { - "timestamp": "2024-07-18T20:09:12.978Z", - "integrations": {}, - "type": "page", - "properties": { - "path": "/src/tests/signals-vanilla/index.html", - "referrer": "", - "search": "", - "title": "Some Title", - "url": "http://my-home.com", - "category": "Retail Page", - "name": "Home" - }, - "category": "Retail Page", - "name": "Home", - "context": { - "__eventOrigin": { - "type": "Signal" - }, - "page": { - "path": "/src/tests/signals-vanilla/index.html", - "referrer": "", - "search": "", - "title": "Some Title", - "url": "http://my-home.com" - }, - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.29 Safari/537.36", - "userAgentData": { - "brands": [], - "mobile": false, - "platform": "" - }, - "locale": "en-US", - "library": { - "name": "analytics.js", - "version": "npm:next-1.72.0" - }, - "timezone": "America/Chicago" - }, - "messageId": "ajs-next-1721333352978-accbdb69-78a4-4357-bd87-e172b42aa3ae", - "anonymousId": "1fb4b6f9-accb-4b69-b8a4-e357fd87e172", - "writeKey": "", - "userId": "john", - "sentAt": "2024-07-18T20:09:13.005Z", - "_metadata": { - "bundled": ["Segment.io"], - "unbundled": [], - "bundledIds": [] - } - }, - { - "timestamp": "2024-07-18T20:09:12.982Z", - "integrations": {}, - "type": "identify", - "userId": "john", - "traits": { - "found": true - }, - "context": { - "__eventOrigin": { - "type": "Signal" - }, - "page": { - "path": "/src/tests/signals-vanilla/index.html", - "referrer": "", - "search": "", - "title": "", - "url": "http://localhost:5432/src/tests/signals-vanilla/index.html" - }, - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.29 Safari/537.36", - "userAgentData": { - "brands": [], - "mobile": false, - "platform": "" - }, - "locale": "en-US", - "library": { - "name": "analytics.js", - "version": "npm:next-1.72.0" - }, - "timezone": "America/Chicago" - }, - "messageId": "ajs-next-1721333352982-db6978a4-e357-4d87-a172-b42aa3aebd8c", - "anonymousId": "1fb4b6f9-accb-4b69-b8a4-e357fd87e172", - "writeKey": "", - "sentAt": "2024-07-18T20:09:13.006Z", - "_metadata": { - "bundled": ["Segment.io"], - "unbundled": [], - "bundledIds": [] - } - }, - { - "timestamp": "2024-07-18T20:09:12.983Z", - "integrations": {}, - "event": "a track call", - "type": "track", - "properties": { - "foo": "bar" - }, - "context": { - "__eventOrigin": { - "type": "Signal" - }, - "page": { - "path": "/src/tests/signals-vanilla/index.html", - "referrer": "", - "search": "", - "title": "", - "url": "http://localhost:5432/src/tests/signals-vanilla/index.html" - }, - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.29 Safari/537.36", - "userAgentData": { - "brands": [], - "mobile": false, - "platform": "" - }, - "locale": "en-US", - "library": { - "name": "analytics.js", - "version": "npm:next-1.72.0" - }, - "timezone": "America/Chicago" - }, - "messageId": "ajs-next-1721333352983-78a4e357-fd87-4172-b42a-a3aebd8c8994", - "userId": "john", - "anonymousId": "1fb4b6f9-accb-4b69-b8a4-e357fd87e172", - "writeKey": "", - "sentAt": "2024-07-18T20:09:13.006Z", - "_metadata": { - "bundled": ["Segment.io"], - "unbundled": [], - "bundledIds": [] - } - }, - { - "timestamp": "2024-07-18T20:09:12.983Z", - "integrations": {}, - "userId": "john", - "type": "alias", - "previousId": "johnsmith", - "context": { - "__eventOrigin": { - "type": "Signal" - }, - "page": { - "path": "/src/tests/signals-vanilla/index.html", - "referrer": "", - "search": "", - "title": "", - "url": "http://localhost:5432/src/tests/signals-vanilla/index.html" - }, - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.29 Safari/537.36", - "userAgentData": { - "brands": [], - "mobile": false, - "platform": "" - }, - "locale": "en-US", - "library": { - "name": "analytics.js", - "version": "npm:next-1.72.0" - }, - "timezone": "America/Chicago" - }, - "messageId": "ajs-next-1721333352983-e357fd87-e172-442a-a3ae-bd8c89946a1a", - "anonymousId": "1fb4b6f9-accb-4b69-b8a4-e357fd87e172", - "writeKey": "", - "sentAt": "2024-07-18T20:09:13.007Z", - "_metadata": { - "bundled": ["Segment.io"], - "unbundled": [], - "bundledIds": [] - } - }, - { - "timestamp": "2024-07-18T20:09:12.984Z", - "integrations": {}, - "type": "group", - "traits": { - "hello": "world" - }, - "groupId": "foo", - "context": { - "__eventOrigin": { - "type": "Signal" - }, - "page": { - "path": "/src/tests/signals-vanilla/index.html", - "referrer": "", - "search": "", - "title": "", - "url": "http://localhost:5432/src/tests/signals-vanilla/index.html" - }, - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.29 Safari/537.36", - "userAgentData": { - "brands": [], - "mobile": false, - "platform": "" - }, - "locale": "en-US", - "library": { - "name": "analytics.js", - "version": "npm:next-1.72.0" - }, - "timezone": "America/Chicago" - }, - "messageId": "ajs-next-1721333352984-fd87e172-b42a-43ae-bd8c-89946a1a7091", - "userId": "john", - "anonymousId": "1fb4b6f9-accb-4b69-b8a4-e357fd87e172", - "writeKey": "", - "sentAt": "2024-07-18T20:09:13.007Z", - "_metadata": { - "bundled": ["Segment.io"], - "unbundled": [], - "bundledIds": [] - } - } -] diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/top-level-metadata.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/top-level-metadata.test.ts deleted file mode 100644 index 02f22caea..000000000 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/top-level-metadata.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { test, expect } from '@playwright/test' -import { waitForCondition } from '../../helpers/playwright-utils' -import { IndexPage } from './index-page' - -const basicEdgeFn = ` - // this is a process signal function - globalThis.processSignal = (signal) => { - if (signal.type === 'interaction') { - analytics.track('hello', { myAnonId: signal.anonymousId, myTimestamp: signal.timestamp }) - } - } -` - -let indexPage: IndexPage - -const isoDateRegEx = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ - -test.beforeEach(async ({ page }) => { - indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn) -}) - -test('Signals should have anonymousId and timestamp at top level', async () => { - await indexPage.network.mockTestRoute() - await indexPage.network.makeFetchCall() - await Promise.all([ - indexPage.clickButton(), - indexPage.makeAnalyticsPageCall(), - indexPage.waitForSignalsApiFlush(), - indexPage.waitForTrackingApiFlush(), - ]) - - const types = [ - 'network', - 'interaction', - 'instrumentation', - 'navigation', - ] as const - - const evs = types.map((type) => ({ - type, - networkCalls: indexPage.signalsAPI.getEvents(type), - })) - - evs.forEach((events) => { - if (!events.networkCalls.length) { - throw new Error(`No events found for type ${events.type}`) - } - events.networkCalls.forEach((event) => { - const expected = { - anonymousId: event.anonymousId, - timestamp: expect.stringMatching(isoDateRegEx), - type: event.properties!.type, - } - expect(event.properties).toMatchObject(expected) - }) - }) - - const getCreatedEvent = () => - indexPage.trackingAPI - .getEvents() - .find((el) => el.type === 'track' && el.event === 'hello') - - await waitForCondition(() => !!getCreatedEvent(), { - errorMessage: 'No track events found, should have an event', - }) - const event = getCreatedEvent()! - expect(event.properties).toEqual({ - myAnonId: event.anonymousId, - myTimestamp: expect.stringMatching(isoDateRegEx), - }) -}) diff --git a/packages/signals/signals-integration-tests/tsconfig.json b/packages/signals/signals-integration-tests/tsconfig.json deleted file mode 100644 index 1ce6b26c7..000000000 --- a/packages/signals/signals-integration-tests/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "exclude": ["node_modules", "dist", "playwright-report"], - "compilerOptions": { - "jsx": "react", - "module": "esnext", - "target": "ES2022", - "moduleResolution": "node", - "lib": ["es2020"] - } -} diff --git a/packages/signals/signals-integration-tests/webpack.config.ts b/packages/signals/signals-integration-tests/webpack.config.ts deleted file mode 100644 index 874319e8e..000000000 --- a/packages/signals/signals-integration-tests/webpack.config.ts +++ /dev/null @@ -1,53 +0,0 @@ -import path from 'path' -import globby from 'globby' -import type { Configuration as WebpackConfiguration } from 'webpack' - -// This config is for bundling fixtures in order to serve the pages that webdriver.io will use in its tests. -const files = globby.sync('src/tests/*/index.bundle.{ts,tsx}', { - cwd: __dirname, -}) - -// e.g if file is src/tests/signals-vanilla/index.bundle.ts, then entry is { signals-vanilla: src/tests/signals-vanilla/index.bundle.ts } -const entries = files.reduce((acc, file) => { - const [dirName] = file.split('/').slice(-2) - const base = path.basename(dirName) - return { - ...acc, - [base]: path.resolve(__dirname, file), - } -}, {}) - -const config: WebpackConfiguration = { - stats: 'minimal', - performance: { - hints: false, - }, - mode: 'production', - devtool: 'source-map', - entry: entries, - output: { - filename: '[name].bundle.js', - path: path.resolve(__dirname, 'dist'), - chunkFilename: '[name].chunk.js', - clean: true, - }, - target: ['web'], - module: { - rules: [ - { - test: /\.(ts|tsx)$/, - use: 'babel-loader', - exclude: /node_modules/, - }, - { - test: /\.css$/i, - use: ['style-loader', 'css-loader'], - }, - ], - }, - resolve: { - extensions: ['.ts', '.js', '.tsx'], - }, -} - -export default config diff --git a/packages/signals/signals/.eslintrc.js b/packages/signals/signals/.eslintrc.js deleted file mode 100644 index eedf2f3c3..000000000 --- a/packages/signals/signals/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type { import('eslint').Linter.Config } */ -module.exports = { - extends: ['../../../.eslintrc'], - env: { - browser: true, - }, -} diff --git a/packages/signals/signals/.lintstagedrc.js b/packages/signals/signals/.lintstagedrc.js deleted file mode 100644 index 7fa7ceee1..000000000 --- a/packages/signals/signals/.lintstagedrc.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - ...require("@internal/config").lintStagedConfig, - 'src/lib/workerbox/*.{js,ts,html}': ['yarn workerbox'] -} - diff --git a/packages/signals/signals/CHANGELOG.md b/packages/signals/signals/CHANGELOG.md deleted file mode 100644 index fa0bf646c..000000000 --- a/packages/signals/signals/CHANGELOG.md +++ /dev/null @@ -1,287 +0,0 @@ -# @segment/analytics-signals - -## 1.13.1 - -### Patch Changes - -- [#1287](https://github.com/segmentio/analytics-next/pull/1287) [`093ecf65`](https://github.com/segmentio/analytics-next/commit/093ecf65c9327fa7ac4753e55620819841d4f43c) Thanks [@silesky](https://github.com/silesky)! - Update default filter to exclude webpack and other HMR mechanisms - -## 1.13.0 - -### Minor Changes - -- [#1284](https://github.com/segmentio/analytics-next/pull/1284) [`64835418`](https://github.com/segmentio/analytics-next/commit/64835418f9dc6cc16fbc6fa7580c565dfb3a446a) Thanks [@silesky](https://github.com/silesky)! - Fix CSP errors with sandboxStrategy: global - -* [#1284](https://github.com/segmentio/analytics-next/pull/1284) [`64835418`](https://github.com/segmentio/analytics-next/commit/64835418f9dc6cc16fbc6fa7580c565dfb3a446a) Thanks [@silesky](https://github.com/silesky)! - Update max signals in buffer to 100 - -## 1.12.1 - -### Patch Changes - -- [#1274](https://github.com/segmentio/analytics-next/pull/1274) [`a00fa28d`](https://github.com/segmentio/analytics-next/commit/a00fa28d76af890f1e2adb69ea0b0860beafca15) Thanks [@silesky](https://github.com/silesky)! - Add a changedProperties array to navigation signals - -- Updated dependencies [[`a00fa28d`](https://github.com/segmentio/analytics-next/commit/a00fa28d76af890f1e2adb69ea0b0860beafca15)]: - - @segment/analytics-signals-runtime@1.5.1 - -## 1.12.0 - -### Minor Changes - -- [#1263](https://github.com/segmentio/analytics-next/pull/1263) [`6753b4b4`](https://github.com/segmentio/analytics-next/commit/6753b4b40d5378734b51369510e707e813a0ad5e) Thanks [@silesky](https://github.com/silesky)! - Add anonymousID and timestamp to signals - -### Patch Changes - -- Updated dependencies [[`6753b4b4`](https://github.com/segmentio/analytics-next/commit/6753b4b40d5378734b51369510e707e813a0ad5e)]: - - @segment/analytics-signals-runtime@1.5.0 - -## 1.11.1 - -### Patch Changes - -- [#1246](https://github.com/segmentio/analytics-next/pull/1246) [`ee838db5`](https://github.com/segmentio/analytics-next/commit/ee838db5b361fc52a400ab2fb8bae50bff4d262b) Thanks [@silesky](https://github.com/silesky)! - Fix argument resolver bug where the following would not set the correct options: - ```ts - analytics.page( - null, - 'foo', - { url: "https://foo.com" }, - { context: { __eventOrigin: { type: 'Signal' } } // would not be set correctly - ) - ``` -- Updated dependencies [[`d5829da8`](https://github.com/segmentio/analytics-next/commit/d5829da8ce6a1664d9be2e00960791d929ee73bc)]: - - @segment/analytics-signals-runtime@1.4.0 - -## 1.11.0 - -### Minor Changes - -- [#1238](https://github.com/segmentio/analytics-next/pull/1238) [`0596bc45`](https://github.com/segmentio/analytics-next/commit/0596bc455b9ecf8ed179f1be5decb0a4b89bb9a5) Thanks [@silesky](https://github.com/silesky)! - Add page data to web signals - -### Patch Changes - -- Updated dependencies [[`0596bc45`](https://github.com/segmentio/analytics-next/commit/0596bc455b9ecf8ed179f1be5decb0a4b89bb9a5)]: - - @segment/analytics-signals-runtime@1.3.0 - -## 1.10.3 - -### Patch Changes - -- [#1233](https://github.com/segmentio/analytics-next/pull/1233) [`9b470331`](https://github.com/segmentio/analytics-next/commit/9b470331584e02dd883942ae83300c9eb971bc95) Thanks [@silesky](https://github.com/silesky)! - Fix max buffer size - -* [#1232](https://github.com/segmentio/analytics-next/pull/1232) [`91dab927`](https://github.com/segmentio/analytics-next/commit/91dab9273954bc26dbcb579a387787f5a0cc185e) Thanks [@silesky](https://github.com/silesky)! - Fix circular submit error for react-hook-form - -## 1.10.2 - -### Patch Changes - -- [#1230](https://github.com/segmentio/analytics-next/pull/1230) [`2fc749a1`](https://github.com/segmentio/analytics-next/commit/2fc749a17b14b2667df76ecce685aefb6656eaae) Thanks [@silesky](https://github.com/silesky)! - Update license key - -- Updated dependencies [[`2fc749a1`](https://github.com/segmentio/analytics-next/commit/2fc749a17b14b2667df76ecce685aefb6656eaae)]: - - @segment/analytics-signals-runtime@1.2.1 - -## 1.10.1 - -### Patch Changes - -- [#1226](https://github.com/segmentio/analytics-next/pull/1226) [`e0ed6a5a`](https://github.com/segmentio/analytics-next/commit/e0ed6a5a072bcb859a2ae304e572e03284d262de) Thanks [@silesky](https://github.com/silesky)! - Testing CI - -## 1.10.0 - -### Minor Changes - -- [#1220](https://github.com/segmentio/analytics-next/pull/1220) [`bf868573`](https://github.com/segmentio/analytics-next/commit/bf8685737466cb1193a54f99871ec7348b8616d8) Thanks [@silesky](https://github.com/silesky)! - Allow registration of middleware to allow for dropping and modification of signals - - ```ts - class MyMiddleware implements SignalsMiddleware { - process(signal: Signal) { - if ( - signal.type === 'network' && - signal.data.action === 'request' && - ... - ) { - // drop or modify signal - return null - } else { - return signal - } - } - } - const signalsPlugin = new SignalsPlugin({ - middleware: [new MyMiddleware()] - }) - ``` - -### Patch Changes - -- [#1224](https://github.com/segmentio/analytics-next/pull/1224) [`171080cc`](https://github.com/segmentio/analytics-next/commit/171080cc9ca198b9f89a9e9154c2a78ed8ef29ee) Thanks [@silesky](https://github.com/silesky)! - Refactor to use SignalEmitter middleware + subscriber interface internally - -## 1.9.2 - -### Patch Changes - -- [#1215](https://github.com/segmentio/analytics-next/pull/1215) [`9a8b0e03`](https://github.com/segmentio/analytics-next/commit/9a8b0e0322a4291a3ee3c5c06974a0af9ea5469f) Thanks [@silesky](https://github.com/silesky)! - Fix bug where in vanilla React environments, the onChange events would error due to circular references. - -## 1.9.1 - -### Patch Changes - -- [#1204](https://github.com/segmentio/analytics-next/pull/1204) [`8e0162b9`](https://github.com/segmentio/analytics-next/commit/8e0162b9553419448b7975337a53fa1c66e70d47) Thanks [@silesky](https://github.com/silesky)! - Update types - -## 1.9.0 - -### Minor Changes - -- [#1202](https://github.com/segmentio/analytics-next/pull/1202) [`00a736f3`](https://github.com/segmentio/analytics-next/commit/00a736f31326328e91c9cae0b244b9db9b0791fc) Thanks [@silesky](https://github.com/silesky)! - - Add support for interaction signals for custom components and elements with contenteditable property - - Allow custom disallow list to override network signals, even if same domain. - -### Patch Changes - -- Updated dependencies [[`00a736f3`](https://github.com/segmentio/analytics-next/commit/00a736f31326328e91c9cae0b244b9db9b0791fc)]: - - @segment/analytics-signals-runtime@1.2.0 - -## 1.8.0 - -### Minor Changes - -- [#1190](https://github.com/segmentio/analytics-next/pull/1190) [`46e88198`](https://github.com/segmentio/analytics-next/commit/46e88198b2f9d3a835e02fa22317d784c6f71ebf) Thanks [@silesky](https://github.com/silesky)! - \* Clear signal buffer at start of new session - - Prune signalBuffer to maxBufferSize on new session (if different) - - Add sessionStorage storage type - -### Patch Changes - -- [#1192](https://github.com/segmentio/analytics-next/pull/1192) [`3410160c`](https://github.com/segmentio/analytics-next/commit/3410160c30024c292f252802cdb98b6b59fced0c) Thanks [@silesky](https://github.com/silesky)! - Always include headers in network interceptor, even if empty - -* [#1188](https://github.com/segmentio/analytics-next/pull/1188) [`de6f86dc`](https://github.com/segmentio/analytics-next/commit/de6f86dc637dbc49f5bb55c1e44a36a2011c14b9) Thanks [@danieljackins](https://github.com/danieljackins)! - Fix sampleRate check - -- [#1197](https://github.com/segmentio/analytics-next/pull/1197) [`342868cb`](https://github.com/segmentio/analytics-next/commit/342868cb9db7da37d8851dadca4b1b1dc0ecd923) Thanks [@silesky](https://github.com/silesky)! - Update signals license - -- Updated dependencies [[`342868cb`](https://github.com/segmentio/analytics-next/commit/342868cb9db7da37d8851dadca4b1b1dc0ecd923)]: - - @segment/analytics-signals-runtime@1.1.1 - -## 1.7.1 - -### Patch Changes - -- [#1186](https://github.com/segmentio/analytics-next/pull/1186) [`bf85047e`](https://github.com/segmentio/analytics-next/commit/bf85047e971add497d5c9ab72972394b1f27e887) Thanks [@silesky](https://github.com/silesky)! - Add signals logging for events - -## 1.7.0 - -### Minor Changes - -- [#1183](https://github.com/segmentio/analytics-next/pull/1183) [`f50bd0f5`](https://github.com/segmentio/analytics-next/commit/f50bd0f5fc30840af33992107cb0a5da432a0b1b) Thanks [@silesky](https://github.com/silesky)! - add support for analytics.reset() - -* [#1184](https://github.com/segmentio/analytics-next/pull/1184) [`ccc97f1b`](https://github.com/segmentio/analytics-next/commit/ccc97f1b61f90c6e07154e205d79952fc579fae1) Thanks [@silesky](https://github.com/silesky)! - Update logging - -## 1.6.0 - -### Minor Changes - -- [#1181](https://github.com/segmentio/analytics-next/pull/1181) [`6fff4114`](https://github.com/segmentio/analytics-next/commit/6fff4114fb2cc9267362d8a3812ad96ec85a1dac) Thanks [@silesky](https://github.com/silesky)! - Add HTTPMethods. Do not gate requests based on content-type. - -### Patch Changes - -- Updated dependencies [[`6fff4114`](https://github.com/segmentio/analytics-next/commit/6fff4114fb2cc9267362d8a3812ad96ec85a1dac)]: - - @segment/analytics-signals-runtime@1.1.0 - -## 1.5.1 - -### Patch Changes - -- [#1179](https://github.com/segmentio/analytics-next/pull/1179) [`ed7a749b`](https://github.com/segmentio/analytics-next/commit/ed7a749be7cddcbf656ac9f72e444ea9f822a718) Thanks [@silesky](https://github.com/silesky)! - Redact formData - -## 1.5.0 - -### Minor Changes - -- [#1176](https://github.com/segmentio/analytics-next/pull/1176) [`7d5d9753`](https://github.com/segmentio/analytics-next/commit/7d5d9753509d8af8f10486c91505b30d2c6e240a) Thanks [@silesky](https://github.com/silesky)! - Update max buffer size to 50 - -* [#1177](https://github.com/segmentio/analytics-next/pull/1177) [`11a943e2`](https://github.com/segmentio/analytics-next/commit/11a943e29e73189c613f93b268e10a64f2561fbc) Thanks [@silesky](https://github.com/silesky)! - - Fix runtime errors for submit - - Add better form submit data - - Loosen content-type to parse text/plain - - Tweak disallow list - - Add labels - -- [#1166](https://github.com/segmentio/analytics-next/pull/1166) [`9e6db285`](https://github.com/segmentio/analytics-next/commit/9e6db2857798f4b5bfdbbfe3570b3d4d83294a79) Thanks [@danieljackins](https://github.com/danieljackins)! - Add sampling logic and block non debug traffic - -* [#1168](https://github.com/segmentio/analytics-next/pull/1168) [`ba2f2b16`](https://github.com/segmentio/analytics-next/commit/ba2f2b165bf1b997a9ce79d410690d27d50378fd) Thanks [@silesky](https://github.com/silesky)! - Refactor runtime to use `@segment/analytics-signals-runtime` - -### Patch Changes - -- [#1178](https://github.com/segmentio/analytics-next/pull/1178) [`08e45530`](https://github.com/segmentio/analytics-next/commit/08e4553001da146f1d80a9b620aef0ef0db04bd4) Thanks [@silesky](https://github.com/silesky)! - \* Refactor disallowList logic to never allow api.segment.io - - Update README - - Export SignalsPlugin in umd as well as global -- Updated dependencies [[`ba2f2b16`](https://github.com/segmentio/analytics-next/commit/ba2f2b165bf1b997a9ce79d410690d27d50378fd)]: - - @segment/analytics-signals-runtime@1.0.0 - -## 1.4.0 - -### Minor Changes - -- [#1164](https://github.com/segmentio/analytics-next/pull/1164) [`c8ce6144`](https://github.com/segmentio/analytics-next/commit/c8ce6144b31bddfc66961e979d5648fb66e102e5) Thanks [@silesky](https://github.com/silesky)! - Emit network signals that result in errors if the response has been emitted. Add ok and status to network signal data. - -* [#1163](https://github.com/segmentio/analytics-next/pull/1163) [`29d17003`](https://github.com/segmentio/analytics-next/commit/29d1700303d0384fbd01edee9e9ff231f35de9ef) Thanks [@silesky](https://github.com/silesky)! - Loosen signal redaction (from https://github.com/segmentio/analytics-next/pull/1106) - -## 1.3.0 - -### Minor Changes - -- [#1155](https://github.com/segmentio/analytics-next/pull/1155) [`29e856f9`](https://github.com/segmentio/analytics-next/commit/29e856f9f36088a0dc625014ebda8e09fc3b621e) Thanks [@silesky](https://github.com/silesky)! - - Fix npm installation esm error by vendoring esm-only module workerbox - -## 1.2.0 - -### Minor Changes - -- [#1152](https://github.com/segmentio/analytics-next/pull/1152) [`101e8414`](https://github.com/segmentio/analytics-next/commit/101e841404e5f55f53ba014b6195bf1066aeb67e) Thanks [@silesky](https://github.com/silesky)! - - normalize XHR URL, http methods, etc - -### Patch Changes - -- [#1151](https://github.com/segmentio/analytics-next/pull/1151) [`571386f5`](https://github.com/segmentio/analytics-next/commit/571386f5d388ed3ff44520ee94795424378950ed) Thanks [@silesky](https://github.com/silesky)! - - Clean up up innerText AND textContent artifacts to make easier to parse. - - Add textContent field - - Make button Clicks more reliable - -## 1.1.0 - -### Minor Changes - -- [#1147](https://github.com/segmentio/analytics-next/pull/1147) [`784ddf21`](https://github.com/segmentio/analytics-next/commit/784ddf21906a2a72c1ccea41d0ba323e189c4010) Thanks [@silesky](https://github.com/silesky)! - Update network signals to add support for allow/disallow - -### Patch Changes - -- [#1150](https://github.com/segmentio/analytics-next/pull/1150) [`04a7cc85`](https://github.com/segmentio/analytics-next/commit/04a7cc85247bdcdb832d0cca4ddbb4391ccada3a) Thanks [@silesky](https://github.com/silesky)! - Support XHR interception - -## 1.0.1 - -### Patch Changes - -- [#1140](https://github.com/segmentio/analytics-next/pull/1140) [`549b028`](https://github.com/segmentio/analytics-next/commit/549b02898dd7c0541957659da8c56e93129507df) Thanks [@silesky](https://github.com/silesky)! - Make network signals more permissive - -## 1.0.0 - -### Major Changes - -- [#1132](https://github.com/segmentio/analytics-next/pull/1132) [`17432de`](https://github.com/segmentio/analytics-next/commit/17432de7b09d543c29f12c48ea61edf73aa7f4a1) Thanks [@silesky](https://github.com/silesky)! - Update signal request/response to lowercase. - -## 0.1.1 - -### Patch Changes - -- [#1116](https://github.com/segmentio/analytics-next/pull/1116) [`0fdf170`](https://github.com/segmentio/analytics-next/commit/0fdf1704af80c168113733beac3ef4eedeab6d2b) Thanks [@silesky](https://github.com/silesky)! - Process signals that occur before analytics init - -## 0.1.0 - -### Minor Changes - -- [#1110](https://github.com/segmentio/analytics-next/pull/1110) [`8d06af2`](https://github.com/segmentio/analytics-next/commit/8d06af29658b579e347ee8dbe39d6f62f01eab05) Thanks [@silesky](https://github.com/silesky)! - Add enums - -### Patch Changes - -- [#1113](https://github.com/segmentio/analytics-next/pull/1113) [`2d89b1d`](https://github.com/segmentio/analytics-next/commit/2d89b1db2413d5c38f6fdb4832d111cd9141a51e) Thanks [@silesky](https://github.com/silesky)! - Update network signals to include a full url - -* [#1115](https://github.com/segmentio/analytics-next/pull/1115) [`73ac593`](https://github.com/segmentio/analytics-next/commit/73ac593226159423b2f63cac190eebd347bbb75a) Thanks [@silesky](https://github.com/silesky)! - Fix group and identify calls. - -- [#1112](https://github.com/segmentio/analytics-next/pull/1112) [`1f68f0e`](https://github.com/segmentio/analytics-next/commit/1f68f0e3309e291fb37f3732d8c32bd55f526633) Thanks [@silesky](https://github.com/silesky)! - Allow collecting signals from sources without an edge function written yet - -## 0.0.1 - -### Patch Changes - -- [#1101](https://github.com/segmentio/analytics-next/pull/1101) [`aee18d2`](https://github.com/segmentio/analytics-next/commit/aee18d222ddfb2273399987fabf92b54876f5e88) Thanks [@silesky](https://github.com/silesky)! - Initial release. diff --git a/packages/signals/signals/LICENSE b/packages/signals/signals/LICENSE deleted file mode 100644 index 73b54b76c..000000000 --- a/packages/signals/signals/LICENSE +++ /dev/null @@ -1,324 +0,0 @@ -Twilio Software Development Kit License Agreement 2.0 - -Notice to user: THIS IS A TWILIO SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT 2.0 -BETWEEN YOU AND TWILIO FOR ACCESS TO AND USE OF TWILIO'S SOFTWARE DEVELOPMENT -KIT. BY USING THIS SOFTWARE DEVELOPMENT KIT, YOU ACCEPT ALL THE TERMS AND -CONDITIONS OF THIS TWILIO SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT 2.0. - -As a courtesy, below is a quick summary of how this Twilio Software Development -Kit License Agreement 2.0 applies when you download, use, or otherwise access -this Twilio Software Development Kit. The full version can be found by scrolling -down and is the only one that is legally controlling and binding. - -Summary - -When you download, use, or otherwise access this software development kit: - -* Twilio grants you a limited license to use this software development kit only - for the purposes of developing your own applications and software and - distributing those applications and software in connection with your use of - Twilio's products and services as a Twilio customer. - -* You agree not to use this software development kit to create a competing - product or service to this software development kit. - -* You agree that Twilio may make changes or updates to this software development - kit (including discontinuing support for and/or the availability of this - software development kit) at any time for any reason. - -* You agree that you will not use this software development kit in any way that - interferes with, disrupts, damages or otherwise affects anyone's servers, - networks, or services (including Twilio's). - -* You agree that any feedback you provide to Twilio regarding this software - development kit, including any suggestions for improvements, belong to Twilio - without any compensation to you and that Twilio owns all legal rights to that - feedback. - -* Except for this software development kit, any use of Twilio's products and - services is governed by a separate agreement between you and Twilio. - -Full Version - -1. Introduction - - a. This Twilio Software Development Kit License Agreement 2.0 (this - "Agreement") accompanies the Twilio Software Development Kit for the - software and related explanatory materials (including the Software and - Documentation, the "SDK") and includes any upgrades, modified versions, - updates, additions, and copies of the SDK licensed to you by Twilio. Twilio - and you may be referred to herein collectively as the "parties" or - individually as a "party." - - b. "Documentation" means Twilio's user manuals, handbooks, installation - guides, and any explanatory materials relating to or accompanying the SDK - provided by Twilio to you either electronically or in hard copy form. - - c. "Intellectual Property Rights" means any and all rights under patent law, - copyright law, trade secret law, trademark law, and any and all other - proprietary rights. - - d. "Software" means the object and source code, and/or other original works of - authorship in the SDK provided by Twilio to you, including all associated - example code, other tools and any upgrades, modified versions, updates, - additions, and copies provided to you pursuant to this Agreement. - - e. "Twilio" means Twilio Inc., organized under the laws of the State of - Delaware, USA, and operating under the laws of the USA with its principal - place of business at 101 Spear Street, 5th Floor, San Francisco, CA 94105. - -2. Accepting this Agreement - - a. In order to use the SDK, you must first agree to this Agreement. You may - not use the SDK if you do not accept this Agreement. By using the SDK, you - hereby agree to the terms of this Agreement. You may not use the SDK and - may not accept this Agreement if you are a person barred from receiving the - SDK under the laws of the United States or other countries, including the - country in which you are a resident or from which you use the SDK. If you - are agreeing to be bound by this Agreement on behalf of your employer or - other entity, you represent and warrant that you have full legal authority - to bind your employer or such entity to this Agreement. If you do not have - the requisite authority, you may not accept this Agreement or use the SDK - on behalf of your employer or other entity. You must use the SDK in - conjunction with Twilio's products and services ("Twilio Services"), and - your use of the Twilio Services will solely be governed by the Twilio Terms - of Service available at https://www.twilio.com/legal/tos or a separate - written agreement entered into between you and Twilio (each, a "Services - Agreement"). This Agreement will, in no way, modify or affect the terms of - the applicable Services Agreement. - -3. SDK License from Twilio - - a. Subject to the terms and conditions of this Agreement, Twilio grants you a - non-exclusive, non-sublicensable, non-assignable, non-transferable, - worldwide, royalty-free, access to and license to use the SDK solely (i) to - copy, display, perform, modify, and create derivative works from the SDK - only for the purpose of internal development of your software products - (each, an "Application") solely for an Application's use in conjunction - with the Twilio Services; and (ii) distribute the Software as part of an - Application, provided that you have a Services Agreement with Twilio for - the use of such Twilio Services. The license granted in Section 3(a)(ii) - will survive the termination of this Agreement, except to the extent you - are in material breach of any of the obligations hereunder. - - b. You are responsible and liable for all use of the SDK, directly or - indirectly, whether such access or use is permitted by or in violation of - this Agreement. Twilio has no liability to you or any third party arising - or resulting from your use of the SDK. - - c. Use, reproduction, and distribution of software dependencies and components - of the SDK licensed under an open source software license are governed - solely by the terms of that open source software license and not this - Agreement. You understand and acknowledge that such open source software is - not licensed to you pursuant to the provisions of this Agreement and that - this Agreement may not be construed to grant any such right and/or - license. If you do not agree to abide by the applicable terms for such - components of the SDK licensed under an open source software license, then - you should not use the SDK. - - d. You will not remove, obscure, or alter any proprietary rights notices - (including copyright and trademark notices) that may be affixed to or - contained within the SDK. - -4. Use of the SDK by You - - a. You may not use the SDK to develop a competing product or service to the - SDK or for any purposes not expressly permitted by this Agreement - including, but not limited to, using the SDK in any manner or for any - purpose that infringes, misappropriates, or otherwise violates any - Intellectual Property Right or other right of any person, or that violates - any applicable law or regulation (including any laws regarding the export - of data or software to and from the United States or other relevant - countries). - - b. You acknowledge and agree that Twilio has no obligation to provide - updates, upgrades, or any other modifications to the SDK. In addition, you - acknowledge and agree that Twilio has no obligation to support or maintain - the SDK. - - c. You agree that the form and nature of the SDK that Twilio provides may - change without prior notice to you and that future versions of the SDK may - be incompatible with Applications developed on previous versions of the - SDK. You agree that Twilio may stop (permanently or temporarily) providing - the SDK (or any features within the SDK) to you or to users generally at - Twilio's sole discretion, without prior notice to you. - - d. You agree that you will not engage in any activity with the SDK, - including the development or distribution of an Application, that - interferes with, disrupts, damages, or accesses in an unauthorized manner - the servers, networks, or other properties or services of any third party - including, but not limited to, Twilio or any telecommunications provider. - - e. Nothing in this Agreement gives you a right to use any of Twilio's trade - names, trademarks, service marks, logos, domain names, or other - distinctive brand features. - - f. You agree that you are solely responsible for (and that Twilio has no - responsibility to you or to any third party for) any breach of your - obligations under this Agreement, any applicable third party contract, or - any applicable law or regulation, and for the consequences (including any - loss or damage which Twilio or any third party may suffer) of any such - breach. - -5. Intellectual Property Ownership and Feedback - - a. You agree that Twilio or third parties own all legal right, title, and - interest in and to the SDK, including any Intellectual Property Rights - that subsist in the SDK. Twilio reserves all rights not expressly granted - to you in this Agreement. Except for the limited rights and licenses - expressly granted under this Agreement, nothing in this Agreement grants, - by implication, waiver, estoppel, or otherwise, to your or any third - party any Intellectual Property Rights or other right, title, or interest - in or to the SDK. - - b. Twilio agrees that it obtains no right, title or interest from you (or - your licensors) under this Agreement in or to any Applications that you - develop using the SDK, including any Intellectual Property Rights that - subsist in those Applications. - - c. If you or any of your employees or contractors send or transmit any - communications, materials, code, documentation, or other original works of - authorship (including any modifications) to Twilio by any form of - electronic or written communication, including but not limited to mail, - email, telephone, source code control systems, issue tracking systems, or - otherwise, suggesting or recommending changes to the SDK, including - without limitation, new features or functionality relating thereto, or any - comments, questions, suggestions, or the like ("Feedback"), Twilio is free - to use such Feedback irrespective of any other obligation or limitation - between the parties governing such Feedback. You hereby assign to Twilio - on your behalf, and on behalf of your employees, contractors and/or - agents, all right, title, and interest in, and Twilio is free to use, - Feedback without any attribution or compensation to any party, any ideas, - know-how, concepts, techniques, or other Intellectual Property Rights - contained in the Feedback, for any purpose whatsoever. Twilio is not - required to use any Feedback. For clarity, Feedback will not include any - code, documentation, or works of authorship, including the Intellectual - Property Rights embedded therein, which are (a) unrelated to the core - functionality of the Software or (b) conceived and/or developed by you (i) - prior to the date this Agreement is accepted by you, (ii) outside the - scope of this Agreement, or (iii) independently without the use of the - SDK. - -6. Terminating this Agreement - - a. This Agreement, as may be updated from time to time, will commence on the - date it is accepted by you and continue until terminated as set out below. - - b. If you want to terminate this Agreement, you may do so by ceasing your - use of the SDK. - - c. Without prejudice to any other rights, this Agreement shall terminate - automatically without notice from Twilio if: - i. your Services Agreement for use of the Twilio Services terminates; or - ii. you have breached any provision of this Agreement; or - iii. Twilio is required to do so by law or regulation; or - iv. Twilio decides to no longer provide the SDK or certain parts of the - SDK to users in the country in which you are resident or from which - you use the service, or the provision of the SDK or certain SDK - services to you by Twilio is, in Twilio's sole discretion, no longer - commercially viable. - - d. When this Agreement terminates, all of the legal rights, obligations, and - liabilities that you and Twilio have benefited from, been subject to (or - which have accrued over time whilst this Agreement has been in force) or - which are expressed to continue indefinitely, will be unaffected by this - cessation. - -7. DISCLAIMER OF WARRANTIES - - a. YOU EXPRESSLY UNDERSTAND AND AGREE THAT YOUR USE OF THE SDK IS AT YOUR - SOLE RISK AND THAT THE SDK IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT - WARRANTY OF ANY KIND FROM TWILIO. - - b. YOUR USE OF THE SDK AND ANY MATERIAL DOWNLOADED OR OTHERWISE OBTAINED - THROUGH THE USE OF THE SDK IS AT YOUR OWN DISCRETION AND RISK AND YOU ARE - SOLELY RESPONSIBLE FOR ANY DAMAGE TO YOUR COMPUTER SYSTEM OR OTHER DEVICE - OR LOSS OF DATA THAT RESULTS FROM SUCH USE. - - c. TWILIO FURTHER EXPRESSLY DISCLAIMS ALL WARRANTIES AND CONDITIONS OF ANY - KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE - IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, FITNESS FOR A - PARTICULAR PURPOSE AND NON-INFRINGEMENT. - -8. LIMITATION OF LIABILITY - - a. YOU EXPRESSLY UNDERSTAND AND AGREE THAT TWILIO, ITS SUBSIDIARIES AND - AFFILIATES, AND ITS LICENSORS WILL NOT BE LIABLE TO YOU UNDER ANY THEORY - OF LIABILITY FOR ANY DAMAGES WHATSOEVER, INCLUDING, BUT NOT LIMITED TO, - DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR EXEMPLARY - DAMAGES THAT MAY BE INCURRED BY YOU, INCLUDING ANY LOSS OF DATA, WHETHER - OR NOT TWILIO OR ITS REPRESENTATIVES HAVE BEEN ADVISED OF OR SHOULD HAVE - BEEN AWARE OF THE POSSIBILITY OF ANY SUCH DAMAGES OR LOSSES ARISING. - -9. Indemnification - - a. To the maximum extent permitted by law, you agree to defend, indemnify - and hold harmless Twilio, its affiliates and their respective directors, - officers, employees, and agents from and against any and all claims, - actions, demands, suits, or proceedings, as well as any and all losses, - liabilities, damages, costs and expenses (including reasonable attorneys' - fees) arising out of or accruing from (a) your use of the SDK or any - resulting Application you develop on the SDK; (b) any Application you - develop using the SDK that infringes any copyright, trademark, trade - secret, trade dress, patent, or other intellectual property right of any - person or defames any person or violates their rights of publicity or - privacy; and (c) your non-compliance with the terms of this Agreement. - -10. Changes to this Agreement - - a. Twilio may make changes to this Agreement as it distributes new versions - of the SDK. When these changes are made, Twilio will make a new version - of this Agreement available on the website where the SDK is made - available. - -11. Additional Terms - - a. Except as provided in this Agreement, this Agreement supersedes all prior - and contemporaneous agreements, oral and written, in relation to the SDK. - No oral or written information or advice given by Twilio, its agents or - employees will create a warranty or in any way increase the scope of the - warranties or obligations under this Agreement. Except as permitted in - Section 10 of this Agreement, no modification to this Agreement will be - legally binding unless set forth in writing and signed by you and Twilio. - - b. You agree that Twilio's failure to enforce at any time any provision of - this Agreement or any other of your obligations does not waive Twilio's - right to do so later. And, if Twilio does expressly waive any provision - of this Agreement or any of your other obligations, that does not mean it - is waived for all time in the future. Any waiver must be in writing and - signed by you and Twilio to be legally binding. - - c. If any court of law, having the jurisdiction to decide on this matter, - rules that any provision of this Agreement is invalid, then that - provision will be removed from this Agreement without affecting the rest - of this Agreement. The remaining provisions of this Agreement will - continue to be valid and enforceable. - - d. You acknowledge and agree that each member of the group of companies of - which Twilio is the parent will be third party beneficiaries to this - Agreement and that such other companies will be entitled to directly - enforce, and rely upon, any provision of this Agreement that confers a - benefit on (or rights in favor of) them. Other than this, no other - person or company will be third party beneficiaries to this Agreement. - - e. THE SDK IS SUBJECT TO UNITED STATES EXPORT LAWS AND REGULATIONS. YOU - MUST COMPLY WITH ALL DOMESTIC AND INTERNATIONAL EXPORT LAWS AND - REGULATIONS THAT APPLY TO THE SDK. THESE LAWS INCLUDE RESTRICTIONS ON - DESTINATIONS, END USERS, AND END USE. - - f. You will not assign or otherwise transfer this Agreement, in whole or in - part, without Twilio's prior written consent. Any attempt by you to - assign, delegate, or transfer this Agreement will be void. Twilio may - assign this Agreement, in whole or in part, without consent. Subject to - this Section 13(f), this Agreement will be binding on both you and - Twilio and both parties' successors and assigns. - - g. This Agreement will be governed by and interpreted according to the laws - of the State of California without regard to conflicts of laws and - principles that would cause laws of another jurisdiction to apply. This - Agreement will not be governed by the United Nations Convention on - Contracts for the International Sale of Goods. Any legal suit, action or - proceeding arising out of or related to this Agreement or the SDK will be - instituted in either the state or federal courts of San Francisco, - California, and the parties each consent to the personal jurisdiction of - these courts. diff --git a/packages/signals/signals/jest.config.js b/packages/signals/signals/jest.config.js deleted file mode 100644 index 82f32aa3f..000000000 --- a/packages/signals/signals/jest.config.js +++ /dev/null @@ -1,6 +0,0 @@ -const { createJestTSConfig } = require('@internal/config') - -module.exports = createJestTSConfig(__dirname, { - testEnvironment: 'jsdom', - setupFilesAfterEnv: ['./jest.setup.ts'], -}) diff --git a/packages/signals/signals/jest.setup.ts b/packages/signals/signals/jest.setup.ts deleted file mode 100644 index edbfd7c3f..000000000 --- a/packages/signals/signals/jest.setup.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* eslint-disable no-undef */ -import './src/test-helpers/jest-extended' -import { JestSerializers } from '@internal/test-helpers' -import 'fake-indexeddb/auto' -globalThis.structuredClone = (v: any) => JSON.parse(JSON.stringify(v)) -expect.addSnapshotSerializer(JestSerializers.jestSnapshotSerializerTimestamp) diff --git a/packages/signals/signals/package.json b/packages/signals/signals/package.json deleted file mode 100644 index cc8bed496..000000000 --- a/packages/signals/signals/package.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "name": "@segment/analytics-signals", - "version": "1.13.1", - "license": "Twilio Software Development Kit License Agreement 2.0", - "main": "./dist/cjs/index.js", - "repository": { - "directory": "packages/signals/signals", - "type": "git", - "url": "https://github.com/segmentio/analytics-next" - }, - "module": "./dist/esm/index.js", - "types": "./dist/types/index.d.ts", - "sideEffects": [ - "./dist/umd/analytics-signals.umd.js" - ], - "jsdelivr": "./dist/umd/analytics-signals.umd.js", - "unpkg": "./dist/umd/analytics-signals.umd.js", - "files": [ - "LICENSE", - "dist/", - "src/", - "!**/__tests__/**", - "!**/test-helpers/**", - "!*.tsbuildinfo" - ], - "scripts": { - ".": "yarn run -T turbo run --filter=@segment/analytics-signals...", - "..": "yarn run -T turbo run --filter=...@segment/analytics-signals...", - "test": "yarn jest", - "lint": "yarn concurrently 'yarn:eslint .' 'yarn:tsc --noEmit'", - "build": "rm -rf dist && yarn concurrently 'yarn:build:*'", - "build:esm": "yarn tsc -p tsconfig.build.json", - "build:cjs": "yarn tsc -p tsconfig.build.json --outDir ./dist/cjs --module commonjs", - "build:bundle": "NODE_ENV=production yarn run webpack", - "workerbox": "node scripts/build-workerbox.js", - "assert-generated": "sh scripts/assert-workerbox-built.sh", - "watch": "rm -rf dist && yarn concurrently 'yarn build:bundle --watch' 'yarn build:esm --watch'", - "version": "sh scripts/version.sh", - "watch:test": "yarn test --watch", - "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": { - "@segment/analytics-generic-utils": "1.2.0", - "@segment/analytics-signals-runtime": "2.0.0", - "idb": "^8.0.0", - "tslib": "^2.4.1" - }, - "peerDependencies": { - "@segment/analytics-next": ">=1.78.0" - }, - "peerDependenciesMeta": { - "@segment/analytics-next": { - "optional": true - } - }, - "packageManager": "yarn@3.4.1", - "devDependencies": { - "@internal/config-webpack": "workspace:^", - "@internal/test-helpers": "workspace:^", - "fake-indexeddb": "^6.0.0", - "node-fetch": "^2.6.7" - } -} diff --git a/packages/signals/signals/scripts/assert-workerbox-built.sh b/packages/signals/signals/scripts/assert-workerbox-built.sh deleted file mode 100644 index d5550d409..000000000 --- a/packages/signals/signals/scripts/assert-workerbox-built.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -# A CI script to ensure people remember to rebuild workerbox related files if workerbox changes - -node scripts/build-workerbox.js -# Check for changes in the workerbox directory -changed_files=$(git diff --name-only | grep 'lib/workerbox') - -# Check for changes in the workerbox directory -if [ -n "$changed_files" ]; then - echo "Error: Changes detected in the workerbox directory. Please commit the changed files:" - echo "$changed_files" - exit 1 -else - echo "Files have not changed" - exit 0 -fi \ No newline at end of file diff --git a/packages/signals/signals/scripts/build-workerbox.js b/packages/signals/signals/scripts/build-workerbox.js deleted file mode 100644 index d1bc1da3b..000000000 --- a/packages/signals/signals/scripts/build-workerbox.js +++ /dev/null @@ -1,64 +0,0 @@ -const fs = require('fs') -const esbuild = require('esbuild') -const path = require('path') - -// Note: This was adopted from the https://github.com/markwylde/workerbox/blob/master/build.js -console.log('Building workerbox...') - -const DEBUG = process.env.DEBUG === 'true' -if (DEBUG) console.log('Minification off.') - -async function writeFileWithDirs(filePath, data) { - // Extract the directory path from the file path - const dir = path.dirname(filePath) - - // Ensure the directory exists - await fs.promises.mkdir(dir, { recursive: true }) - - // Write the file - await fs.promises.writeFile(filePath, data, 'utf8') -} - -async function build() { - console.log(new Date(), 'rebuilding...') - - // clean up dist folder - await fs.promises.rm('./src/lib/workerbox/dist', { - recursive: true, - force: true, - }) - - await esbuild.build({ - entryPoints: ['./src/lib/workerbox/worker.ts'], - bundle: true, - outfile: './src/lib/workerbox/dist/worker.js', - minify: !DEBUG, - }) - - const jsData = await fs.promises.readFile( - './src/lib/workerbox/dist/worker.js', - 'utf8' - ) - - const TEMPLATE_PLACEHOLDER = `{{WORKERSCRIPT}}` - const htmlData = ( - await fs.promises.readFile('./src/lib/workerbox/worker.html', 'utf8') - ).replace(TEMPLATE_PLACEHOLDER, jsData) - - await writeFileWithDirs('./src/lib/workerbox/dist/worker.html', htmlData) - await writeFileWithDirs( - './src/lib/workerbox/worker.generated.ts', - [ - '/* eslint-disable */', - `// built from /dist/worker.html`, - `export default atob('${Buffer.from(htmlData).toString('base64')}');`, - ].join('\n') - ) -} - -build() - .then(() => console.log('Build successful')) - .catch((err) => { - console.error(err) - process.exit(1) - }) diff --git a/packages/signals/signals/scripts/version.sh b/packages/signals/signals/scripts/version.sh deleted file mode 100644 index 9cc9c0766..000000000 --- a/packages/signals/signals/scripts/version.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -# Generate a version.ts file from the version in the package.json - -PKG_VERSION=$(node --eval="process.stdout.write(require('./package.json').version)") - -cat <src/generated/version.ts -// This file is generated. -export const version = '$PKG_VERSION' -EOF - -git add src/generated/version.ts diff --git a/packages/signals/signals/src/core/analytics-service/__tests__/analytics-service.test.ts b/packages/signals/signals/src/core/analytics-service/__tests__/analytics-service.test.ts deleted file mode 100644 index 021ab9581..000000000 --- a/packages/signals/signals/src/core/analytics-service/__tests__/analytics-service.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { analyticsMock } from '../../../test-helpers/mocks' -import { SegmentEventStub } from '../../../types' -import { AnalyticsService } from '../index' - -describe(AnalyticsService, () => { - const mockAnalyticsInstance = { - ...analyticsMock, - settings: { - ...analyticsMock.settings, - writeKey: 'foo', - }, - } - - let service: AnalyticsService - beforeEach(() => { - service = new AnalyticsService(mockAnalyticsInstance) - }) - - it('should return the correct write key', () => { - expect(service.instance.settings.writeKey).toBe('foo') - }) - - describe('createSegmentInstrumentationEventGenerator', () => { - it('should register and emit signals for non-Signal events', async () => { - const generator = service.createSegmentInstrumentationEventGenerator() - const mockSignalEmitter = { emit: jest.fn() } - const mockEvent: SegmentEventStub = { - type: 'track', - context: { - foo: 123, - }, - } - - await generator.register(mockSignalEmitter as any) - - const middleware = - mockAnalyticsInstance.addSourceMiddleware.mock.calls[0][0] - middleware({ payload: { obj: mockEvent }, next: jest.fn() }) - - expect(mockSignalEmitter.emit).toHaveBeenCalledTimes(1) - const call = mockSignalEmitter.emit.mock.calls[0][0] - delete call.data.page - expect(call).toMatchInlineSnapshot(` - { - "anonymousId": "", - "context": { - "library": { - "name": "@segment/analytics-next", - "version": "0.0.0", - }, - "signalsRuntime": "", - }, - "data": { - "rawEvent": { - "context": { - "foo": 123, - }, - "type": "track", - }, - "type": "track", - }, - "index": undefined, - "timestamp": , - "type": "instrumentation", - } - `) - }) - it('should not emit signals if the event is a Signal event', async () => { - const generator = service.createSegmentInstrumentationEventGenerator() - const mockSignalEmitter = { emit: jest.fn() } - const mockEvent: SegmentEventStub = { - type: 'track', - context: { - __eventOrigin: { - type: 'Signal', - }, - }, - } - - await generator.register(mockSignalEmitter as any) - - const middleware = - mockAnalyticsInstance.addSourceMiddleware.mock.calls[0][0] - middleware({ payload: { obj: mockEvent }, next: jest.fn() }) - - expect(mockSignalEmitter.emit).not.toHaveBeenCalled() - }) - - it('should not emit signals after disable is called', async () => { - const generator = service.createSegmentInstrumentationEventGenerator() - const mockSignalEmitter = { emit: jest.fn() } - const mockEvent: SegmentEventStub = { - type: 'track', - context: { - __eventOrigin: { - type: 'Signal', - }, - }, - } - - const disable = await generator.register(mockSignalEmitter as any) - disable() - - const middleware = - mockAnalyticsInstance.addSourceMiddleware.mock.calls[0][0] - middleware({ payload: { obj: mockEvent }, next: jest.fn() }) - - expect(mockSignalEmitter.emit).not.toHaveBeenCalled() - }) - }) -}) diff --git a/packages/signals/signals/src/core/analytics-service/index.ts b/packages/signals/signals/src/core/analytics-service/index.ts deleted file mode 100644 index 8d3b51320..000000000 --- a/packages/signals/signals/src/core/analytics-service/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { AnyAnalytics, CDNSettings } from '../../types' -import { SignalGenerator } from '../signal-generators/types' -import { createInstrumentationSignal } from '../../types/factories' - -type EdgeFunctionSettings = { downloadURL: string; version?: number } - -/** - * Helper / facade that wraps the analytics, and abstracts away the details of the analytics instance. - */ -export class AnalyticsService { - instance: AnyAnalytics - edgeFnSettings?: EdgeFunctionSettings - constructor(analyticsInstance: AnyAnalytics) { - this.instance = analyticsInstance - this.edgeFnSettings = this.parseEdgeFnSettings( - analyticsInstance.settings.cdnSettings - ) - } - - private parseEdgeFnSettings( - cdnSettings: CDNSettings - ): EdgeFunctionSettings | undefined { - const edgeFnSettings = cdnSettings.edgeFunction - if (edgeFnSettings && 'downloadURL' in edgeFnSettings) { - return edgeFnSettings - } - } - - createSegmentInstrumentationEventGenerator(): SignalGenerator { - let disable = false - const generator: SignalGenerator = { - id: 'segment-event-generator', - register: async (signalEmitter) => { - await this.instance.addSourceMiddleware(({ payload, next }) => { - if (disable) { - return - } - const event = payload.obj - - const isEventFromSignalEdgeFunction = - event.context.__eventOrigin?.type === 'Signal' - - if (!isEventFromSignalEdgeFunction) { - signalEmitter.emit(createInstrumentationSignal(event)) - } - - return next(payload) - }) - return () => { - disable = true - } - }, - } - return generator - } -} diff --git a/packages/signals/signals/src/core/buffer/__tests__/buffer.test.ts b/packages/signals/signals/src/core/buffer/__tests__/buffer.test.ts deleted file mode 100644 index 54a07d6f3..000000000 --- a/packages/signals/signals/src/core/buffer/__tests__/buffer.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { sleep } from '@segment/analytics-core' -import { createMockTarget } from '../../../test-helpers/mocks/factories' -import { range } from '../../../test-helpers/range' -import { createInteractionSignal } from '../../../types/factories' -import { getSignalBuffer, SignalBuffer } from '../index' - -const createMockSignal = () => - createInteractionSignal({ - eventType: 'submit', - target: createMockTarget({ - id: Math.random().toString(), - }), - }) - -describe(getSignalBuffer, () => { - let buffer: SignalBuffer - beforeEach(async () => { - sessionStorage.clear() - buffer = getSignalBuffer({ - maxBufferSize: 10, - }) - await buffer.clear() - }) - describe('indexDB', () => { - it('should instantiate without throwing an error', () => { - expect(buffer).toBeTruthy() - }) - it('should add and clear', async () => { - const mockSignal = createInteractionSignal({ - eventType: 'submit', - target: createMockTarget({}), - }) - await buffer.add(mockSignal) - await expect(buffer.getAll()).resolves.toEqual([mockSignal]) - await buffer.clear() - await expect(buffer.getAll()).resolves.toHaveLength(0) - }) - - it('should delete older signals when maxBufferSize is exceeded', async () => { - const signals = range(15).map(() => createMockSignal()) - - for (const signal of signals) { - await buffer.add(signal) - } - - const storedSignals = await buffer.getAll() - expect(storedSignals).toHaveLength(10) - expect(storedSignals).toEqual(signals.slice(-10).reverse()) - }) - - it('should delete older signals on initialize if current number exceeds maxBufferSize', async () => { - const signals = range(15).map((_) => createMockSignal()) - - for (const signal of signals) { - await buffer.add(signal) - } - - // Re-initialize buffer - buffer = getSignalBuffer({ - maxBufferSize: 10, - }) - - const storedSignals = await buffer.getAll() - expect(storedSignals).toHaveLength(10) - expect(storedSignals).toEqual(signals.slice(-10).reverse()) - }) - - it('should clear signal buffer if there is a new session according to session storage', async () => { - const mockSignal = createMockSignal() - await buffer.add(mockSignal) - await expect(buffer.getAll()).resolves.toEqual([mockSignal]) - - // Simulate a new session by clearing session storage and re-initializing the buffer - sessionStorage.clear() - await sleep(100) - buffer = getSignalBuffer({ - maxBufferSize: 10, - }) - - await expect(buffer.getAll()).resolves.toHaveLength(0) - }) - }) - describe('sessionStorage', () => { - it('should instantiate without throwing an error', () => { - expect(buffer).toBeTruthy() - }) - - it('should add and clear', async () => { - const mockSignal = createMockSignal() - await buffer.add(mockSignal) - await expect(buffer.getAll()).resolves.toEqual([mockSignal]) - await buffer.clear() - await expect(buffer.getAll()).resolves.toHaveLength(0) - }) - - it('should delete older signals when maxBufferSize is exceeded', async () => { - const signals = range(15).map(() => createMockSignal()) - - for (const signal of signals) { - await buffer.add(signal) - } - - const storedSignals = await buffer.getAll() - expect(storedSignals).toHaveLength(10) - expect(storedSignals).toEqual(signals.slice(-10).reverse()) - }) - }) -}) diff --git a/packages/signals/signals/src/core/buffer/index.ts b/packages/signals/signals/src/core/buffer/index.ts deleted file mode 100644 index 8f85c8ad3..000000000 --- a/packages/signals/signals/src/core/buffer/index.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { Signal } from '@segment/analytics-signals-runtime' -import { openDB, DBSchema, IDBPDatabase, IDBPObjectStore } from 'idb' -import { logger } from '../../lib/logger' -import { WebStorage } from '../../lib/storage/web-storage' - -interface SignalDatabase extends DBSchema { - signals: { - key: string - value: Signal - } -} - -export interface SignalPersistentStorage { - getAll(): Promise | Signal[] - add(value: Signal): Promise | void - clear(): void -} - -interface IDBPDatabaseSignals extends IDBPDatabase {} -interface IDBPObjectStoreSignals - extends IDBPObjectStore< - SignalDatabase, - ['signals'], - 'signals', - 'readonly' | 'readwrite' | 'versionchange' - > {} - -const MAX_BUFFER_SIZE_DEFAULT = 100 - -interface StoreSettings { - maxBufferSize?: number -} -export class SignalStoreIndexDB implements SignalPersistentStorage { - static readonly DB_NAME = 'Segment Signals Buffer' - static readonly STORE_NAME = 'signals' - private db: Promise - private maxBufferSize: number - private sessionKeyStorage = new WebStorage(window.sessionStorage) - static deleteDatabase() { - return indexedDB.deleteDatabase(SignalStoreIndexDB.DB_NAME) - } - - async getStore( - permission: IDBTransactionMode, - database?: IDBPDatabaseSignals - ): Promise { - const db = database ?? (await this.db) - const store = db - .transaction(SignalStoreIndexDB.STORE_NAME, permission) - .objectStore(SignalStoreIndexDB.STORE_NAME) - return store - } - - constructor(settings: StoreSettings) { - this.maxBufferSize = settings.maxBufferSize ?? MAX_BUFFER_SIZE_DEFAULT - this.db = this.initSignalDB() - } - - private async initSignalDB(): Promise { - const db = await openDB(SignalStoreIndexDB.DB_NAME, 1, { - upgrade(db) { - db.createObjectStore(SignalStoreIndexDB.STORE_NAME, { - autoIncrement: true, - }) - }, - }) - logger.debug('Signals Buffer (indexDB) initialized') - // if the signal buffer is too large, delete the oldest signals (e.g, the settings have changed) - const store = await this.getStore('readwrite', db) - await this.clearStoreIfNeeded(store) - await this.countAndDeleteOldestIfNeeded(store, true) - await store.transaction.done - return db - } - - private async clearStoreIfNeeded(store: IDBPObjectStoreSignals) { - // prevent the signals buffer from persisting across sessions (e.g, user closes tab and reopens) - const sessionKey = 'segment_signals_db_session_key' - if (!sessionStorage.getItem(sessionKey)) { - this.sessionKeyStorage.setItem(sessionKey, true) - await store.clear!() - logger.debug('New Session, so signals buffer cleared') - } - } - - async add(signal: Signal): Promise { - const store = await this.getStore('readwrite') - await store.add!(signal) - await this.countAndDeleteOldestIfNeeded(store) - return store.transaction.done - } - - private async countAndDeleteOldestIfNeeded( - store: IDBPObjectStoreSignals, - deleteMultiple = false - ): Promise { - let count = await store.count() - if (count > this.maxBufferSize) { - const cursor = await store.openCursor() - if (cursor) { - // delete up to maxItems - if (deleteMultiple) { - while (count > this.maxBufferSize) { - await cursor.delete!() - await cursor.continue() - count-- - } - logger.debug( - `Signals Buffer: Purged signals to max buffer size of ${this.maxBufferSize}` - ) - } else { - // just delete the oldest item - await cursor.delete!() - count-- - } - } - } - } - - /** - * Get list of signals from the store, with the newest signals first. - */ - async getAll(): Promise { - const store = await this.getStore('readonly') - const signals = await store.getAll() - await store.transaction.done - return signals.reverse() - } - - async clear(): Promise { - const store = await this.getStore('readwrite') - await store.clear!() - await store.transaction.done - } -} - -export class SignalStoreSessionStorage implements SignalPersistentStorage { - private readonly storageKey = 'segment_signals_buffer' - private maxBufferSize: number - - constructor(settings: StoreSettings) { - this.maxBufferSize = settings.maxBufferSize ?? MAX_BUFFER_SIZE_DEFAULT - } - - add(signal: Signal): void { - const signals = this.getAll() - signals.unshift(signal) - if (signals.length > this.maxBufferSize) { - // delete the last one - signals.splice(-1) - } - sessionStorage.setItem(this.storageKey, JSON.stringify(signals)) - } - - clear(): void { - sessionStorage.removeItem(this.storageKey) - } - - getAll(): Signal[] { - const signals = sessionStorage.getItem(this.storageKey) - return signals ? JSON.parse(signals) : [] - } -} - -export class SignalBuffer< - T extends SignalPersistentStorage = SignalPersistentStorage -> { - public store: T - constructor(store: T) { - this.store = store - } - async add(signal: Signal) { - try { - return await this.store.add(signal) - } catch (e) { - console.error(e) - return undefined - } - } - async getAll() { - try { - return await this.store.getAll() - } catch (e) { - console.error(e) - return [] - } - } - async clear() { - try { - return await this.store.clear() - } catch (e) { - console.error(e) - return undefined - } - } -} - -export interface SignalBufferSettingsConfig< - T extends SignalPersistentStorage = SignalPersistentStorage -> { - /** - * Maximum number of signals to store. Only applies if no custom storage implementation is provided. - */ - maxBufferSize?: number - /** - * Choose between sessionStorage and indexDB. Only applies if no custom storage implementation is provided. - * @default 'indexDB' - */ - storageType?: 'session' | 'indexDB' - /** - * Custom storage implementation - * @default SignalStoreIndexDB - */ - signalStorage?: T -} -export const getSignalBuffer = < - T extends SignalPersistentStorage = SignalPersistentStorage ->( - settings: SignalBufferSettingsConfig -) => { - const store = - settings.signalStorage ?? settings.storageType === 'session' - ? new SignalStoreSessionStorage(settings) - : new SignalStoreIndexDB(settings) - return new SignalBuffer(store) -} diff --git a/packages/signals/signals/src/core/debug-mode/index.ts b/packages/signals/signals/src/core/debug-mode/index.ts deleted file mode 100644 index 1f60764f6..000000000 --- a/packages/signals/signals/src/core/debug-mode/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * If query string is set, coerce to boolean - * If query string is not set, return undefined - */ -export const parseDebugModeQueryString = (): boolean | undefined => { - const queryParams = new URLSearchParams(window.location.search) - - const val = - queryParams.get('segment_signals_debug') || - queryParams.get('seg_signals_debug') - if (val === 'true' || val === 'false') { - return val === 'true' - } - return undefined -} - -/** - * This turns on advanced logging for signals! - */ -export type LogLevelOptions = 'info' | 'debug' | 'off' -export const parseSignalsLogLevel = (): LogLevelOptions | undefined => { - const queryParams = new URLSearchParams(window.location.search) - - const val = - queryParams.get('segment_signals_log_level') || - queryParams.get('seg_signals_log_level') - if (val === 'info' || val === 'debug' || val === 'off') { - return val - } else if (typeof val === 'string') { - console.error( - `Invalid signals_log_level: "${val}". Valid options are: info, debug, off` - ) - } - return undefined -} diff --git a/packages/signals/signals/src/core/emitter/__tests__/signal-emitter.test.ts b/packages/signals/signals/src/core/emitter/__tests__/signal-emitter.test.ts deleted file mode 100644 index a49d4c1ad..000000000 --- a/packages/signals/signals/src/core/emitter/__tests__/signal-emitter.test.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { sleep } from '@segment/analytics-core' -import { Signal } from '@segment/analytics-signals-runtime' -import { - SignalEmitter, - SignalsMiddlewareContext, - SignalsSubscriber, - SignalsMiddleware, -} from '../index' - -const mockCtx = { - unstableGlobalSettings: {}, - analyticsInstance: {}, - buffer: {}, -} as SignalsMiddlewareContext - -describe(SignalEmitter, () => { - let emitter: SignalEmitter - let mockSubscriber: jest.Mocked - let mockSignal: Signal - let mockMiddleware: jest.Mocked - - beforeEach(() => { - emitter = new SignalEmitter() - mockSubscriber = { - load: jest.fn(), - process: jest.fn(), - } - mockSignal = { type: 'test', data: {} } as any as Signal - mockMiddleware = { - load: jest.fn(), - process: jest.fn().mockReturnValue(mockSignal), - } - }) - - it('should subscribe and unsubscribe a subscriber', async () => { - emitter.subscribe(mockSubscriber) - await emitter.start(mockCtx) - - emitter.emit(mockSignal) - await sleep(0) - expect(mockSubscriber.process).toHaveBeenCalledTimes(1) - expect(mockSubscriber.process).toHaveBeenCalledWith(mockSignal) - await emitter.unsubscribe(mockSubscriber) - - emitter.emit(mockSignal) - - expect(mockSubscriber.process).toHaveBeenCalledTimes(1) - }) - - it('should subscribe and unsubscribe if subscriber is a function', async () => { - const mockSubscriber = jest.fn() - emitter.subscribe(mockSubscriber) - - emitter.emit(mockSignal) - - expect(mockSubscriber).not.toHaveBeenCalledWith(mockSignal) - - await emitter.start(mockCtx) - - expect(mockSubscriber).toHaveBeenCalledTimes(1) - expect(mockSubscriber).toHaveBeenCalledWith(mockSignal) - await emitter.unsubscribe(mockSubscriber) - - emitter.emit(mockSignal) - - expect(mockSubscriber).toHaveBeenCalledTimes(1) - }) - - it('should allow subscribing after start is called', async () => { - await emitter.start(mockCtx) - - const mockSubscriber = jest.fn() - emitter.subscribe(mockSubscriber) - - emitter.emit(mockSignal) - expect(mockSubscriber).toHaveBeenCalledTimes(1) - expect(mockSubscriber).toHaveBeenCalledWith(mockSignal) - }) - - it('should handle multiple subscribers', async () => { - const mockSubscriber2 = { - load: jest.fn(), - process: jest.fn(), - } - emitter.subscribe(mockSubscriber) - emitter.subscribe(mockSubscriber2) - - emitter.emit(mockSignal) - - expect(mockSubscriber.process).not.toHaveBeenCalledWith(mockSignal) - expect(mockSubscriber2.process).not.toHaveBeenCalledWith(mockSignal) - - await emitter.start(mockCtx) - - expect(mockSubscriber.process).toHaveBeenCalledTimes(1) - expect(mockSubscriber.process).toHaveBeenCalledWith(mockSignal) - expect(mockSubscriber2.process).toHaveBeenCalledTimes(1) - expect(mockSubscriber2.process).toHaveBeenCalledWith(mockSignal) - }) - - it('should handle multiple unsubscriptions', async () => { - await emitter.start(mockCtx) - - const anotherSubscriber: SignalsSubscriber = { - load: jest.fn(), - process: jest.fn(), - } - - emitter.subscribe(mockSubscriber, anotherSubscriber) - emitter.unsubscribe(mockSubscriber, anotherSubscriber) - - // Emit a signal to test if neither subscriber receives it - emitter.emit(mockSignal) - - expect(mockSubscriber.process).not.toHaveBeenCalled() - expect(anotherSubscriber.process).not.toHaveBeenCalled() - }) - - it('should buffer signals before initialization', async () => { - emitter.emit(mockSignal) - - expect(mockSubscriber.process).not.toHaveBeenCalled() - - emitter.subscribe(mockSubscriber) - await emitter.start(mockCtx) - - expect(mockSubscriber.process).toHaveBeenCalledWith(mockSignal) - expect(mockSubscriber.process).toHaveBeenCalledTimes(1) - }) - - it('should process signals through middleware', async () => { - emitter = await new SignalEmitter({ middleware: [mockMiddleware] }) - .subscribe(mockSubscriber) - .start(mockCtx) - - emitter.emit(mockSignal) - - await sleep(0) - - expect(mockMiddleware.process).toHaveBeenCalledWith(mockSignal) - expect(mockSubscriber.process).toHaveBeenCalledWith(mockSignal) - }) - - it('should run buffered signals through middleware', async () => { - emitter = new SignalEmitter({ middleware: [mockMiddleware] }).subscribe( - mockSubscriber - ) - - emitter.emit(mockSignal) - - await emitter.start(mockCtx) - - await sleep(0) - - expect(mockMiddleware.process).toHaveBeenCalledWith(mockSignal) - expect(mockSubscriber.process).toHaveBeenCalledWith(mockSignal) - }) - - it('should drop signals if middleware returns null', async () => { - mockMiddleware.process.mockReturnValueOnce(null) - - emitter = await new SignalEmitter({ middleware: [mockMiddleware] }) - .subscribe(mockSubscriber) - .start(mockCtx) - - // drop signal - emitter.emit(mockSignal) - - await sleep(0) - - expect(mockMiddleware.process).toHaveBeenCalledWith(mockSignal) - expect(mockSubscriber.process).not.toHaveBeenCalled() - }) - - it('should not drop signals if middleware returns undefined', async () => { - mockMiddleware.process.mockReturnValueOnce(undefined as any) - - emitter = await new SignalEmitter({ middleware: [mockMiddleware] }) - .subscribe(mockSubscriber) - .start(mockCtx) - - emitter.emit(mockSignal) - - await sleep(0) - - expect(mockMiddleware.process).toHaveBeenCalledWith(mockSignal) - expect(mockSubscriber.process).toHaveBeenCalledWith(mockSignal) - }) - - it('should support .addMiddleware method', async () => { - emitter = await new SignalEmitter() - .addMiddleware(mockMiddleware) - .subscribe(mockSubscriber) - .start(mockCtx) - - emitter.emit(mockSignal) - - await sleep(0) - - expect(mockMiddleware.process).toHaveBeenCalledWith(mockSignal) - expect(mockSubscriber.process).toHaveBeenCalledWith(mockSignal) - }) - - it('should support .removeMiddleware method', async () => { - emitter = await new SignalEmitter() - .addMiddleware(mockMiddleware) - .removeMiddleware(mockMiddleware) - .subscribe(mockSubscriber) - .start(mockCtx) - - emitter.emit(mockSignal) - - await sleep(0) - - expect(mockMiddleware.process).not.toHaveBeenCalled() - expect(mockSubscriber.process).toHaveBeenCalled() - }) - - it('should support add and removeMiddleware after start', async () => { - emitter = await new SignalEmitter().subscribe(mockSubscriber).start(mockCtx) - emitter.addMiddleware(mockMiddleware) - emitter.emit(mockSignal) - - await sleep(0) - - expect(mockMiddleware.process).toHaveBeenCalledTimes(1) - - emitter.removeMiddleware(mockMiddleware) - - emitter.emit(mockSignal) - await sleep(0) - expect(mockMiddleware.process).toHaveBeenCalledTimes(1) - }) - - it('should handle multiple middlewares being added', async () => { - const mockMiddleware2 = { - load: jest.fn(), - process: jest.fn().mockReturnValue(mockSignal), - } - - emitter = await new SignalEmitter() - .addMiddleware(mockMiddleware, mockMiddleware2) - .subscribe(mockSubscriber) - .start(mockCtx) - - emitter.emit(mockSignal) - - await sleep(0) - - expect(mockMiddleware.process).toHaveBeenCalledWith(mockSignal) - expect(mockMiddleware2.process).toHaveBeenCalledWith(mockSignal) - expect(mockSubscriber.process).toHaveBeenCalledWith(mockSignal) - }) - - it('middleware should block the flushing of signals forall .load promises are resolved', async () => { - class MockMiddleware1 implements SignalsMiddleware { - process(signal: Signal): Signal | null { - // @ts-ignore - signal.foo = this.ctx - return signal - } - ctx!: SignalsMiddlewareContext - async load(ctx: SignalsMiddlewareContext): Promise { - await sleep(50) - this.ctx = ctx - } - } - const mockMiddleware1 = new MockMiddleware1() - const processSpy1 = jest.spyOn(mockMiddleware1, 'process') - const loadSpy1 = jest.spyOn(mockMiddleware1, 'load') - - const mockMiddleware2 = - new (class MockMiddleware2 extends MockMiddleware1 {})() - - const processSpy2 = jest.spyOn(mockMiddleware2, 'process') - const loadSpy2 = jest.spyOn(mockMiddleware2, 'load') - - emitter = new SignalEmitter().addMiddleware( - mockMiddleware1, - mockMiddleware2 - ) - - emitter.emit(mockSignal) - - void emitter.start(mockCtx) - - await sleep(0) - - expect(processSpy1).not.toHaveBeenCalled() - expect(processSpy2).not.toHaveBeenCalled() - - await sleep(50) - - expect(loadSpy1).toHaveBeenCalledTimes(1) - expect(loadSpy2).toHaveBeenCalledTimes(1) - - expect(processSpy1).toHaveBeenCalledWith(mockSignal) - expect(processSpy1).toHaveLastReturnedWith( - expect.objectContaining({ foo: mockCtx }) - ) - expect(processSpy2).toHaveBeenCalledWith(mockSignal) - expect(processSpy2).toHaveLastReturnedWith( - expect.objectContaining({ foo: mockCtx }) - ) - }) - - it('subscribers .load methods should NOT block the flushing of signals for other subscribers (only for themselves)', async () => { - class MockSubscriber1 implements SignalsSubscriber { - process(signal: Signal): Signal { - return signal - } - ctx!: SignalsMiddlewareContext - async load(ctx: SignalsMiddlewareContext): Promise { - this.ctx = ctx - } - } - const mockSubscriber1 = new MockSubscriber1() - const processSpy1 = jest.spyOn(mockSubscriber1, 'process') - const loadSpy1 = jest.spyOn(mockSubscriber1, 'load') - - class MockSubscriber2 extends MockSubscriber1 { - _resolveLoadForTest!: () => void - async load(): Promise { - return new Promise((resolve) => { - this._resolveLoadForTest = resolve - }) - } - } - - const mockSubscriber2 = new MockSubscriber2() - const processSpy2 = jest.spyOn(mockSubscriber2, 'process') - const loadSpy2 = jest.spyOn(mockSubscriber2, 'load') - - emitter = new SignalEmitter().subscribe(mockSubscriber1, mockSubscriber2) - - emitter.emit(mockSignal) - - void emitter.start(mockCtx) - - await sleep(0) - - expect(loadSpy1).toHaveBeenCalledTimes(1) - expect(processSpy1).toHaveBeenCalledWith(mockSignal) - // Subscriber 1 loads immediately and Subscriber 2 , but they shouldn't block eachother - expect(loadSpy2).toHaveBeenCalledTimes(1) - expect(processSpy2).not.toHaveBeenCalled() - - // Subscriber 2's load method should be resolved, so it should process the signal - mockSubscriber2._resolveLoadForTest() - await sleep(0) - expect(processSpy2).toHaveBeenCalledTimes(1) - }) -}) diff --git a/packages/signals/signals/src/core/emitter/index.ts b/packages/signals/signals/src/core/emitter/index.ts deleted file mode 100644 index 13f1a91b0..000000000 --- a/packages/signals/signals/src/core/emitter/index.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { Signal } from '@segment/analytics-signals-runtime' -import { AnyAnalytics } from '../../types' -import { SignalBuffer } from '../buffer' -import { SignalGlobalSettings } from '../signals' - -export interface EmitSignal { - emit: (signal: Signal) => void -} - -export interface SignalsMiddlewareContext { - /** - * These are global application settings. They are considered unstable, and should only be used internally. - * @interal - */ - readonly unstableGlobalSettings: SignalGlobalSettings - - /** - * @internal - */ - analyticsInstance: AnyAnalytics - - /** - * @internal - */ - buffer: SignalBuffer -} - -export interface PluginSettings { - writeKey: string -} - -/** - * A middleware is a plugin that modifies or drops signals - */ -export interface SignalsMiddleware { - /** - * Wait for .load to complete before emitting signals - * This blocks the signal emitter until all plugins are loaded. - */ - load(ctx: SignalsMiddlewareContext): Promise | void - process(signal: Signal): Signal | null -} - -/** - * A subscriber is basically a destination -- it receives a signal once it has travelled through the pipeline. - */ -export interface SignalsSubscriber { - /** - * Wait for .load to complete before emitting signals to this subscriber - */ - load(ctx: SignalsMiddlewareContext): Promise | void - process(signal: Signal): void -} - -export type AnySignalSubscriber = SignalsSubscriber | ((signal: Signal) => void) - -/** - * Normalizes all subscribers to a single interface - * Waits for the current plugin to load before emitting signals - * @internal - */ -class SignalsSubscriberAdapter implements SignalsSubscriber { - subscriber: AnySignalSubscriber - private loadedPromise?: Promise - constructor(subscriber: AnySignalSubscriber) { - this.subscriber = subscriber - } - - load(ctx: SignalsMiddlewareContext): void { - if (typeof this.subscriber === 'function') return - this.loadedPromise = Promise.resolve(this.subscriber.load(ctx)) - } - - process(signal: Signal): void { - const sub = this.subscriber - if (typeof sub === 'function') { - sub(signal) - } else { - if (this.loadedPromise) { - void this.loadedPromise.then(() => sub.process(signal)) - } else { - throw new Error('load() must be called before process()') - } - } - } -} - -export interface SignalEmitterSettings { - middleware?: SignalsMiddleware[] -} -export class SignalEmitter implements EmitSignal { - private subscribers = new Set() - private middlewares = new Set() - private signalQueue: Signal[] = [] // Buffer for signals emitted before initialization - private startedCtx?: SignalsMiddlewareContext // Context that .start() is called with. If this is defined, the emitter has been started. - constructor(settings?: SignalEmitterSettings) { - if (settings?.middleware) { - this.addMiddleware(...settings.middleware) - } - } - /** - * Load all middleware, flush the buffer, and enable eager processing - */ - async start( - signalsMiddlewareContext: SignalsMiddlewareContext - ): Promise { - if (this.startedCtx) return this - - // Load all middleware, waiting for all of them to complete their load method before processing any singals - await Promise.all( - [...this.middlewares].map((mw) => mw.load(signalsMiddlewareContext)) - ) - - // Load all destinations/subscribers, but do not wait for their load methods to be invoked, since they are not supposed to modify signals. - this.subscribers.forEach((sub) => { - void sub.load(signalsMiddlewareContext) - }) - - // Enable eager processing of signals - this.startedCtx = signalsMiddlewareContext - - // Flush all buffered signals - while (this.signalQueue.length > 0) { - const signal = this.signalQueue.shift() as Signal - this.processAndEmit(signal) - } - return this - } - - /** - * Enqueue a signal to be processed by all plugins and subscribers - */ - emit(signal: Signal): void { - if (!this.startedCtx) { - // Buffer the signal if not initialized - this.signalQueue.push(signal) - return - } - - // Process and notify listeners - this.processAndEmit(signal) - } - - addMiddleware(...mws: SignalsMiddleware[]): this { - mws.forEach((mw) => { - if (this.startedCtx) { - void mw.load(this.startedCtx) - } - this.middlewares.add(mw) - }) - return this - } - - removeMiddleware(...mws: SignalsMiddleware[]): this { - mws.forEach((mw) => this.middlewares.delete(mw)) - return this - } - - /** - * Listen to signals emitted, once they have travelled through the plugin pipeline. - * This is equivalent to a destination plugin. - */ - subscribe(...subs: AnySignalSubscriber[]): this { - subs - .map((d) => new SignalsSubscriberAdapter(d)) - .forEach((d) => { - if (!this.subscribers.has(d)) { - if (this.startedCtx) { - void d.load(this.startedCtx) - } - this.subscribers.add(d) - } - }) - return this - } - - unsubscribe(...unsubbed: AnySignalSubscriber[]): this { - unsubbed.forEach((toUnsubscribe) => { - const adapter = [...this.subscribers].find( - (s) => s.subscriber === toUnsubscribe - ) - if (adapter) { - this.subscribers.delete(adapter) - } - }) - return this - } - - private processAndEmit(signal: Signal): void { - // Apply plugin; drop the signal if any plugin returns null - for (const middleware of this.middlewares) { - const processed = middleware.process(signal) - if (processed === null) return // Drop the signal - } - - // Process events for subscribers - for (const subscriber of this.subscribers) { - // Emit shallow copy as basic protection against accidental modification - // Subscribers should not modify signals - const signalCopy = { ...signal } - subscriber.process(signalCopy) - } - } -} diff --git a/packages/signals/signals/src/core/middleware/event-processor/index.ts b/packages/signals/signals/src/core/middleware/event-processor/index.ts deleted file mode 100644 index f51f52c73..000000000 --- a/packages/signals/signals/src/core/middleware/event-processor/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Signal } from '@segment/analytics-signals-runtime' -import { logger } from '../../../lib/logger' -import { SignalBuffer } from '../../buffer' -import { SignalsSubscriber, SignalsMiddlewareContext } from '../../emitter' -import { SignalEventProcessor } from '../../processor/processor' -import { - normalizeEdgeFunctionURL, - GlobalScopeSandbox, - WorkerSandbox, - IframeSandboxSettings, - SignalSandbox, - NoopSandbox, -} from '../../processor/sandbox' - -export class SignalsEventProcessorSubscriber implements SignalsSubscriber { - processor!: SignalEventProcessor - buffer!: SignalBuffer - load(ctx: SignalsMiddlewareContext) { - this.buffer = ctx.buffer - const sandboxSettings = ctx.unstableGlobalSettings.sandbox - const normalizedEdgeFunctionURL = normalizeEdgeFunctionURL( - sandboxSettings.functionHost, - sandboxSettings.edgeFnDownloadURL - ) - - let sandbox: SignalSandbox - - if (!normalizedEdgeFunctionURL) { - console.warn( - `No processSignal function found. Have you written a processSignal function on app.segment.com?` - ) - logger.debug('Initializing sandbox: noop') - sandbox = new NoopSandbox() - } else if ( - sandboxSettings.sandboxStrategy === 'iframe' || - sandboxSettings.processSignal - ) { - logger.debug('Initializing sandbox: iframe') - sandbox = new WorkerSandbox( - new IframeSandboxSettings({ - processSignal: sandboxSettings.processSignal, - edgeFnDownloadURL: normalizedEdgeFunctionURL, - }) - ) - } else { - logger.debug('Initializing sandbox: global scope') - sandbox = new GlobalScopeSandbox({ - edgeFnDownloadURL: normalizedEdgeFunctionURL, - }) - } - - this.processor = new SignalEventProcessor(ctx.analyticsInstance, sandbox) - } - async process(signal: Signal) { - return this.processor.process(signal, await this.buffer.getAll()) - } -} diff --git a/packages/signals/signals/src/core/middleware/network-signals-filter/__tests__/network-signals-filter.test.ts b/packages/signals/signals/src/core/middleware/network-signals-filter/__tests__/network-signals-filter.test.ts deleted file mode 100644 index 1305bc881..000000000 --- a/packages/signals/signals/src/core/middleware/network-signals-filter/__tests__/network-signals-filter.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - NetworkSettingsConfig, - NetworkSettingsConfigSettings, - NetworkSignalsFilter, -} from '../network-signals-filter' -import { setLocation } from '../../../../test-helpers/set-location' - -describe(NetworkSignalsFilter, () => { - class TestNetworkSignalsFilter extends NetworkSignalsFilter { - constructor(settings: Partial = {}) { - super(new NetworkSettingsConfig(settings)) - } - } - - beforeEach(() => { - setLocation({ hostname: 'localhost' }) - }) - - it('should respect the allow/disallow list', () => { - const filter = new TestNetworkSignalsFilter({ - networkSignalsAllowList: [ - new RegExp(`allowed.com/api/v1`), - 'foo.com/api/v2', - ], - networkSignalsDisallowList: [new RegExp(`allowed.com/api/v666`)], - }) - - // allowed - expect(filter.isAllowed(`http://allowed.com/api/v1/test`)).toBe(true) - // not allowed - expect(filter.isAllowed(`http://allowed.com/api/v666`)).toBe(false) - }) - - it('will not allow signals for same domain if networkSignalsAllowSameDomain = false', () => { - const filter = new TestNetworkSignalsFilter({ - networkSignalsAllowList: ['foo.com'], - networkSignalsDisallowList: [], - networkSignalsAllowSameDomain: false, - }) - - expect(filter.isAllowed(`http://${window.location.hostname}/test`)).toBe( - false - ) - expect(filter.isAllowed(`http://foo.com/test`)).toBe(true) - }) - - it('disallows the signal if it matches in both the allow and disallow list', () => { - const filter = new TestNetworkSignalsFilter({ - networkSignalsAllowList: ['disallowed-api.com'], - networkSignalsDisallowList: ['https://disallowed-api.com'], - }) - - expect(filter.isAllowed(`https://disallowed-api.com/api/foo`)).toBe(false) - }) - - it('allows an explicit disallow list to override same-domain signals', () => { - const filter = new TestNetworkSignalsFilter({ - networkSignalsDisallowList: ['/foo'], - }) - - expect(filter.isAllowed(`/test/foo`)).toBe(false) - expect(filter.isAllowed(`/test/bar`)).toBe(true) - }) - - it('always disallows segment api network signals', () => { - const filter = new TestNetworkSignalsFilter({ - networkSignalsAllowList: ['.*'], - }) - - expect(filter.isAllowed(`https://api.segment.io`)).toBe(false) - }) -}) diff --git a/packages/signals/signals/src/core/middleware/network-signals-filter/network-signals-filter.ts b/packages/signals/signals/src/core/middleware/network-signals-filter/network-signals-filter.ts deleted file mode 100644 index 8a8e9c649..000000000 --- a/packages/signals/signals/src/core/middleware/network-signals-filter/network-signals-filter.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { Signal } from '@segment/analytics-signals-runtime' -import { RegexLike } from '../../../types/settings' -import { SignalsMiddleware, SignalsMiddlewareContext } from '../../emitter' -import { SignalsSettingsConfig } from '../../signals' -import { isSameDomain } from '../../signal-generators/network-gen/helpers' - -export type NetworkSettingsConfigSettings = Pick< - SignalsSettingsConfig, - | 'networkSignalsAllowList' - | 'networkSignalsAllowSameDomain' - | 'networkSignalsDisallowList' -> -export class NetworkSettingsConfig { - networkSignalsAllowSameDomain: boolean - networkSignalsFilterList: NetworkSignalsFilterList - constructor({ - networkSignalsAllowList, - networkSignalsDisallowList, - networkSignalsAllowSameDomain, - }: NetworkSettingsConfigSettings) { - this.networkSignalsAllowSameDomain = networkSignalsAllowSameDomain ?? true - this.networkSignalsFilterList = new NetworkSignalsFilterList( - networkSignalsAllowList, - networkSignalsDisallowList - ) - } -} - -class NetworkFilterListItem { - regexes: RegexLike[] - private get combinedRegex(): RegExp { - const normalizeRegex = (regex: RegExp | string): RegExp => { - return typeof regex === 'string' ? new RegExp(regex) : regex - } - return new RegExp( - this.regexes - .map((val) => normalizeRegex(val)) - .map((r) => r.source) - .join('|') - ) - } - constructor(regexes: RegexLike[]) { - this.regexes = regexes - } - - test(value: string): boolean { - if (!this.regexes.length) { - return false - } - return this.combinedRegex.test(value) - } - - add(...regex: RegexLike[]) { - this.regexes.push(...regex) - } - - addURLLike(...urlLike: string[]) { - const parsedRegexes = urlLike - .map((domain) => this.urlToRegex(domain)) - .filter((val: T): val is NonNullable => Boolean(val)) - this.add(...parsedRegexes) - } - - private urlToRegex(urlLike: string): RegExp | undefined { - const clean = urlLike.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - try { - return new RegExp(clean) - } catch (e) { - console.error(`Invalid regex: ${clean}`, e) - } - } -} - -export class NetworkSignalsFilterList { - public allowed: NetworkFilterListItem - public disallowed: NetworkFilterListItem - private disallowedDefaults: NetworkFilterListItem - - constructor( - allowList: RegexLike[] | undefined, - disallowList: RegexLike[] | undefined - ) { - this.allowed = new NetworkFilterListItem(allowList || []) - this.disallowed = new NetworkFilterListItem(disallowList || []) - this.disallowedDefaults = new NetworkFilterListItem([ - 'api.segment.io', - 'signals.segment.io', - 'cdn.segment', - 'webpack', - 'hot-update', - '@vite/client', - '@vite/env', - '_hmr', - '-hmr', - 'react-refresh', - 'livereload', - 'browser-sync', - 'esbuild', - ]) - } - - isDisallowed(url: string): boolean { - return this.disallowed.test(url) || this.disallowedDefaults.test(url) - } - - isAllowed(url: string): boolean { - return this.allowed.test(url) - } - - getRegexes() { - return { - allowed: this.allowed.regexes.map((el) => el.toString()), - disallowed: this.disallowed.regexes.map((el) => el.toString()), - } - } -} - -export type NetworkSignalsFilterSettings = Pick< - NetworkSettingsConfig, - 'networkSignalsAllowSameDomain' | 'networkSignalsFilterList' -> -export class NetworkSignalsFilter { - settings: NetworkSignalsFilterSettings - constructor(settings: NetworkSignalsFilterSettings) { - this.settings = settings - } - isAllowed(url: string): boolean { - const { networkSignalsFilterList, networkSignalsAllowSameDomain } = - this.settings - - // anything that is disallowed takes precedence over the allow list. - if (networkSignalsFilterList.isDisallowed(url)) { - return false - } - - const allowed = - // allowed because it's in the allow list - networkSignalsFilterList.isAllowed(url) || - // allowed because it's the same domain - (networkSignalsAllowSameDomain && isSameDomain(url)) - return allowed - } -} - -export class NetworkSignalsFilterMiddleware implements SignalsMiddleware { - private filter!: NetworkSignalsFilter - - load(ctx: SignalsMiddlewareContext): void | Promise { - this.filter = new NetworkSignalsFilter(ctx.unstableGlobalSettings.network) - } - - process(signal: Signal): Signal | null { - if (signal.type === 'network') { - return this.filter.isAllowed(signal.data.url) ? signal : null - } else { - return signal - } - } -} diff --git a/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/client.test.ts b/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/client.test.ts deleted file mode 100644 index 0e651b7c6..000000000 --- a/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/client.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { ISO_TIMESTAMP_REGEX } from '@internal/test-helpers' -import { createSuccess } from '@segment/analytics-next/src/test-helpers/factories' -import unfetch from 'unfetch' -import { getPageData } from '../../../../lib/page-data' -import { - createInstrumentationSignal, - createNetworkSignal, -} from '../../../../types/factories' -import { SignalsIngestClient } from '../signals-ingest-client' - -jest.mock('unfetch') -jest - .mocked(unfetch) - .mockImplementation(() => createSuccess({ integrations: {} })) - -describe(SignalsIngestClient, () => { - let client: SignalsIngestClient - - beforeEach(async () => { - client = new SignalsIngestClient('test', { - shouldIngestSignals: () => true, - }) - }) - - it('makes an instrumentation track call via the analytics api', async () => { - expect(client).toBeTruthy() - const signal = createInstrumentationSignal({ - type: 'track', - }) - const ctx = await client.send(signal) - - expect(ctx!.event.type).toEqual('track') - expect(ctx!.event.properties).toMatchObject({ - anonymousId: expect.any(String), - timestamp: expect.stringMatching(ISO_TIMESTAMP_REGEX), - type: 'instrumentation', - index: 0, - data: { - rawEvent: { - type: 'track', - }, - page: getPageData(), - }, - }) - }) - it('makes a network track call via the analytics api', async () => { - expect(client).toBeTruthy() - const signal = createNetworkSignal({ - requestId: '12345', - contentType: 'application/json', - action: 'request', - body: { - hello: 'how are you', - }, - method: 'POST', - url: 'http://foo.com', - }) - - const ctx = await client.send(signal) - expect(ctx!.event.properties!).toMatchInlineSnapshot(` - { - "anonymousId": "", - "context": { - "library": { - "name": "@segment/analytics-next", - "version": "0.0.0", - }, - "signalsRuntime": "", - }, - "data": { - "action": "request", - "body": { - "hello": "XXX", - }, - "contentType": "application/json", - "method": "POST", - "page": { - "hash": "", - "hostname": "localhost", - "path": "/", - "referrer": "", - "search": "", - "title": "", - "url": "http://localhost/", - }, - "requestId": "12345", - "url": "http://foo.com", - }, - "index": 0, - "timestamp": , - "type": "network", - } - `) - }) -}) diff --git a/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/redact.test.ts b/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/redact.test.ts deleted file mode 100644 index d04a57077..000000000 --- a/packages/signals/signals/src/core/middleware/signals-ingest/__tests__/redact.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { createMockTarget } from '../../../../test-helpers/mocks/factories' -import * as factories from '../../../../types/factories' -import { redactJsonValues, redactSignalData } from '../redact' - -describe(redactJsonValues, () => { - it('should redact string values in an object', () => { - const obj = { name: 'John Doe', age: '30' } - const expected = { name: 'XXX', age: 'XXX' } - expect(redactJsonValues(obj)).toEqual(expected) - }) - - it('should redact string values in a nested object', () => { - const obj = { user: { name: 'Jane Doe', age: '25' }, active: true } - const expected = { - user: { name: 'XXX', age: 'XXX' }, - active: true, - } - expect(redactJsonValues(obj, 1)).toEqual(expected) - }) - it('should not redact null or undefined values', () => { - const obj = { name: 'John Doe', age: null, email: undefined } - const expected = { name: 'XXX', age: null, email: undefined } - expect(redactJsonValues(obj)).toEqual(expected) - }) - - it('should redact bigint values', () => { - const obj = { name: 'John Doe', age: BigInt(30) } - const expected = { name: 'XXX', age: 999 } - expect(redactJsonValues(obj)).toEqual(expected) - }) - - it('should redact boolean values by setting them to true', () => { - const obj = { name: 'John Doe', active: false } - const expected = { name: 'XXX', active: true } - expect(redactJsonValues(obj)).toEqual(expected) - }) - - it('should redact string values in an array', () => { - const arr = ['John Doe', '30'] - const expected = ['XXX', 'XXX'] - expect(redactJsonValues(arr)).toEqual(expected) - }) - - it('should handle mixed types in an array', () => { - const arr = ['Jane Doe', 25, { email: 'jane@example.com' }] - const expected = ['XXX', 999, { email: 'XXX' }] - expect(redactJsonValues(arr, 1)).toEqual(expected) - }) - - it('should not redact if depth is not reached', () => { - const obj = { a: 'A', l2: { b: 'B', l3: { c: 'C', l4: { d: 'D' } } } } - const expected = { - a: 'A', - l2: { b: 'B', l3: { c: 'XXX', l4: { d: 'XXX' } } }, - } - expect(redactJsonValues(obj, 3)).toEqual(expected) - }) -}) - -describe(redactSignalData, () => { - it('should return the signal as is if the type is "instrumentation"', () => { - const signal = factories.createInstrumentationSignal({ - foo: 123, - } as any) - expect(redactSignalData(signal)).toEqual(signal) - }) - - it('should return the signal as is if the type is "userDefined"', () => { - const signal = factories.createUserDefinedSignal({ value: 'secret' }) - expect(redactSignalData(signal)).toEqual(signal) - }) - - it('should redact the value in the "target" property if the type is "interaction"', () => { - const signal = factories.createInteractionSignal({ - eventType: 'change', - change: { value: 'secret' }, - listener: 'onchange', - target: createMockTarget({ - value: 'secret', - formData: { password: '123' }, - }), - }) - const expected = factories.createInteractionSignal({ - eventType: 'change', - change: { value: 'XXX' }, - listener: 'onchange', - target: createMockTarget({ value: 'XXX', formData: { password: 'XXX' } }), - }) - expect(redactSignalData(signal)).toMatchSignal(expected) - }) - - it('should redact attributes in change and in target if the listener is "mutation"', () => { - const signal = factories.createInteractionSignal({ - eventType: 'change', - change: { 'aria-selected': 'value' }, - listener: 'mutation', - target: createMockTarget({ - attributes: { 'aria-selected': 'value', foo: 'value' }, - textContent: 'value', - innerText: 'value', - }), - }) - const expected = factories.createInteractionSignal({ - eventType: 'change', - change: { 'aria-selected': 'XXX' }, - listener: 'mutation', - target: createMockTarget({ - attributes: { 'aria-selected': 'XXX', foo: 'XXX' }, - textContent: 'XXX', - innerText: 'XXX', - }), - }) - expect(redactSignalData(signal)).toMatchSignal(expected) - }) - - it('should redact the textContent and innerText in the "target" property if the listener is "contenteditable"', () => { - const signal = factories.createInteractionSignal({ - eventType: 'change', - listener: 'contenteditable', - change: { textContent: 'secret' }, - target: createMockTarget({ textContent: 'secret', innerText: 'secret' }), - }) - const expected = factories.createInteractionSignal({ - eventType: 'change', - listener: 'contenteditable', - change: { textContent: 'XXX' }, - target: createMockTarget({ textContent: 'XXX', innerText: 'XXX' }), - }) - expect(redactSignalData(signal)).toMatchSignal(expected) - }) - - it('should redact the values in the "data" property if the type is "network"', () => { - const signal = factories.createNetworkSignal({ - requestId: 'foo', - contentType: 'application/json', - action: 'request', - method: 'POST', - url: 'http://foo.com', - body: { name: 'John Doe', age: 30 }, - }) - const expected = factories.createNetworkSignal({ - requestId: 'foo', - contentType: 'application/json', - action: 'request', - method: 'POST', - url: 'http://foo.com', - body: { name: 'XXX', age: 999 }, - }) - expect(redactSignalData(signal)).toMatchSignal(expected) - }) - - it('should not mutate the original signal object', () => { - const originalSignal = factories.createInteractionSignal({ - eventType: 'click', - target: createMockTarget({ value: 'sensitiveData' }), - }) - const originalSignalCopy = JSON.parse(JSON.stringify(originalSignal)) - - redactSignalData(originalSignal) - expect(originalSignal).toEqual(originalSignalCopy) - }) -}) diff --git a/packages/signals/signals/src/core/middleware/signals-ingest/index.ts b/packages/signals/signals/src/core/middleware/signals-ingest/index.ts deleted file mode 100644 index 0f5cf71fb..000000000 --- a/packages/signals/signals/src/core/middleware/signals-ingest/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Signal } from '@segment/analytics-signals-runtime' -import { SignalsSubscriber, SignalsMiddlewareContext } from '../../emitter' -import { SignalsIngestClient } from './signals-ingest-client' - -export class SignalsIngestSubscriber implements SignalsSubscriber { - client!: SignalsIngestClient - ctx!: SignalsMiddlewareContext - load(ctx: SignalsMiddlewareContext) { - this.ctx = ctx - this.client = new SignalsIngestClient( - ctx.analyticsInstance.settings.writeKey, - ctx.unstableGlobalSettings.ingestClient - ) - } - process(signal: Signal) { - void this.client.send(signal) - } -} diff --git a/packages/signals/signals/src/core/middleware/signals-ingest/redact.ts b/packages/signals/signals/src/core/middleware/signals-ingest/redact.ts deleted file mode 100644 index fc782799e..000000000 --- a/packages/signals/signals/src/core/middleware/signals-ingest/redact.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Signal } from '@segment/analytics-signals-runtime' - -/** - * This is a very imperfect redaction. - * Issues: - * - innerText could contain sensitive data, and be leaked depending - */ -export const redactSignalData = (signalArg: Signal): Signal => { - const signal = structuredClone(signalArg) - if (signal.type === 'interaction') { - if ( - 'target' in signal.data && - signal.data.target && - typeof signal.data.target === 'object' - ) { - if ( - 'value' in signal.data.target && - signal.data.target.value !== undefined - ) { - signal.data.target.value = redactJsonValues(signal.data.target.value) - } - if ( - 'checked' in signal.data.target && - signal.data.target.checked !== undefined - ) { - signal.data.target.checked = redactJsonValues( - signal.data.target.checked - ) - } - - if ('formData' in signal.data.target) { - signal.data.target.formData = redactJsonValues( - signal.data.target.formData - ) - } - - if (signal.data.eventType === 'change') { - if ('change' in signal.data) { - signal.data.change = redactJsonValues(signal.data.change) - } - - if (signal.data.listener === 'mutation') { - if ('innerText' in signal.data.target) { - signal.data.target.innerText = redactJsonValues( - signal.data.target.innerText - ) - } - if ('textContent' in signal.data.target) { - signal.data.target.textContent = redactJsonValues( - signal.data.target.textContent - ) - } - if ('attributes' in signal.data.target) { - signal.data.target.attributes = redactJsonValues( - signal.data.target.attributes - ) - } - } - - if (signal.data.listener === 'contenteditable') { - if ('textContent' in signal.data.target) { - signal.data.target.textContent = redactJsonValues( - signal.data.target.textContent - ) - } - if ('innerText' in signal.data.target) { - signal.data.target.innerText = redactJsonValues( - signal.data.target.innerText - ) - } - } - } - } - } else if (signal.type === 'network') { - signal.data.body = redactJsonValues(signal.data.body) - } - return signal -} - -function redactPrimitive(value: unknown) { - const type = typeof value - if (type === 'boolean') { - return true - } - - if (['number', 'bigint'].includes(type)) { - return 999 - } - - if (value === undefined || value === null) { - return value - } - - return 'XXX' -} - -export function redactJsonValues(data: unknown, redactAfterDepth = 0): any { - if (typeof data === 'object' && data) { - if (Array.isArray(data)) { - return data.map((item) => redactJsonValues(item, redactAfterDepth - 1)) - } else { - const redactedData: any = {} - for (const key in data) { - redactedData[key] = redactJsonValues( - // @ts-ignore - data[key], - redactAfterDepth === 0 ? 0 : redactAfterDepth - 1 - ) - } - return redactedData - } - } else if (redactAfterDepth <= 0) { - const ret = redactPrimitive(data) - return ret - } else { - return data - } -} diff --git a/packages/signals/signals/src/core/middleware/signals-ingest/signals-ingest-client.ts b/packages/signals/signals/src/core/middleware/signals-ingest/signals-ingest-client.ts deleted file mode 100644 index 2373feb65..000000000 --- a/packages/signals/signals/src/core/middleware/signals-ingest/signals-ingest-client.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Analytics, segmentio } from '@segment/analytics-next' -import { logger } from '../../../lib/logger' -import { Signal } from '@segment/analytics-signals-runtime' -import { redactSignalData } from './redact' - -export class SignalsIngestSettings { - flushAt: number - flushInterval: number - apiHost: string - shouldDisableSignalsRedaction: () => boolean - shouldIngestSignals: () => boolean - writeKey?: string - constructor(settings: SignalsIngestSettingsConfig) { - this.flushAt = settings.flushAt ?? 5 - this.apiHost = settings.apiHost ?? 'signals.segment.io/v1' - this.flushInterval = settings.flushInterval ?? 2000 - this.shouldDisableSignalsRedaction = - settings.shouldDisableSignalsRedaction ?? (() => false) - this.shouldIngestSignals = settings.shouldIngestSignals ?? (() => false) - } -} - -export interface SignalsIngestSettingsConfig { - apiHost?: string - flushAt?: number - flushInterval?: number - shouldDisableSignalsRedaction?: () => boolean - shouldIngestSignals?: () => boolean -} -/** - * This currently just uses the Segment analytics-next library to send signals. - * This persists the signals in a queue until the client is initialized. - */ -export class SignalsIngestClient { - private settings: SignalsIngestSettings - private analytics: Promise - - /** - * This matters to sort the signals in the UI if the timestamp conflict (which can happen very very rarely) - */ - private index = 0 - - constructor(writeKey: string, settings: SignalsIngestSettingsConfig = {}) { - this.settings = new SignalsIngestSettings(settings) - this.analytics = this.createAnalyticsClient({ writeKey }) - } - - private async createAnalyticsClient(settings: { writeKey: string }) { - const analytics = new Analytics({ writeKey: settings.writeKey }) - this.settings.writeKey = analytics.settings.writeKey - await analytics.register( - segmentio(analytics, { - apiHost: this.settings.apiHost, - apiKey: this.settings.writeKey, - deliveryStrategy: { - config: { - size: this.settings.flushAt, - timeout: this.settings.flushInterval, - }, - strategy: 'batching', - }, - }) - ) - - analytics.emit('initialize', settings) - analytics.on('track', (...args) => { - logger.debug('Track event from analytics client', ...args) - }) - return analytics - } - - private async sendTrackCall(signal: Signal) { - const analytics = await this.analytics - if (!this.settings.shouldIngestSignals()) { - return - } - const disableRedaction = this.settings.shouldDisableSignalsRedaction() - const cleanSignal = disableRedaction ? signal : redactSignalData(signal) - - if (disableRedaction) { - logger.debug('Sending unredacted data to segment', cleanSignal) - } - - const MAGIC_EVENT_NAME = 'Segment Signal Generated' - - return analytics.track(MAGIC_EVENT_NAME, { - ...cleanSignal, - index: this.index++, - }) - } - - send(signal: Signal) { - return this.sendTrackCall(signal) - } -} diff --git a/packages/signals/signals/src/core/middleware/user-info/index.ts b/packages/signals/signals/src/core/middleware/user-info/index.ts deleted file mode 100644 index 61b0403ec..000000000 --- a/packages/signals/signals/src/core/middleware/user-info/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Signal } from '@segment/analytics-signals-runtime' -import { UserInfo } from '../../../types' -import { SignalsMiddleware, SignalsMiddlewareContext } from '../../emitter' - -export class UserInfoMiddleware implements SignalsMiddleware { - user!: UserInfo - - load(ctx: SignalsMiddlewareContext) { - this.user = ctx.analyticsInstance.user() - } - - process(signal: Signal): Signal { - // anonymousId should always exist here unless the user is explicitly setting it to null - signal.anonymousId = this.user.anonymousId() - return signal - } -} diff --git a/packages/signals/signals/src/core/processor/__tests__/sandbox-settings.test.ts b/packages/signals/signals/src/core/processor/__tests__/sandbox-settings.test.ts deleted file mode 100644 index f5c5e9c02..000000000 --- a/packages/signals/signals/src/core/processor/__tests__/sandbox-settings.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { IframeSandboxSettings, IframeSandboxSettingsConfig } from '../sandbox' - -describe(IframeSandboxSettings, () => { - const edgeFnResponseBody = `function processSignal() { console.log('hello world') }` - const baseSettings: IframeSandboxSettingsConfig = { - processSignal: undefined, - edgeFnDownloadURL: 'http://example.com/download', - edgeFnFetchClient: jest.fn().mockReturnValue( - Promise.resolve({ - text: () => edgeFnResponseBody, - }) - ), - } - test('initializes with provided settings', async () => { - const sandboxSettings = new IframeSandboxSettings({ ...baseSettings }) - expect(baseSettings.edgeFnFetchClient).toHaveBeenCalledWith( - baseSettings.edgeFnDownloadURL - ) - expect(await sandboxSettings.processSignal).toEqual(edgeFnResponseBody) - }) - - test('should call edgeFnDownloadURL', async () => { - const settings: IframeSandboxSettingsConfig = { - ...baseSettings, - processSignal: undefined, - edgeFnDownloadURL: 'https://foo.com/download', - } - new IframeSandboxSettings(settings) - expect(baseSettings.edgeFnFetchClient).toHaveBeenCalledWith( - 'https://foo.com/download' - ) - }) - - test('creates default processSignal when parameters are missing', async () => { - const consoleWarnSpy = jest - .spyOn(console, 'warn') - .mockImplementation(() => {}) - const settings: IframeSandboxSettingsConfig = { - ...baseSettings, - processSignal: undefined, - edgeFnDownloadURL: undefined, - } - const sandboxSettings = new IframeSandboxSettings(settings) - expect(await sandboxSettings.processSignal).toMatchInlineSnapshot( - `"globalThis.processSignal = function() {}"` - ) - expect(baseSettings.edgeFnFetchClient).not.toHaveBeenCalled() - expect(consoleWarnSpy).toHaveBeenCalledWith( - expect.stringContaining('processSignal') - ) - }) -}) diff --git a/packages/signals/signals/src/core/processor/arg-resolvers.ts b/packages/signals/signals/src/core/processor/arg-resolvers.ts deleted file mode 100644 index 74abdb901..000000000 --- a/packages/signals/signals/src/core/processor/arg-resolvers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { - resolveAliasArguments, - resolveArguments, - resolvePageArguments, - resolveUserArguments, -} from '@segment/analytics-next' - -export const resolvers = { - resolveAliasArguments, - resolveArguments, - resolvePageArguments, - resolveUserArguments: resolveUserArguments({ - id: () => undefined, - }), -} diff --git a/packages/signals/signals/src/core/processor/polyfills.ts b/packages/signals/signals/src/core/processor/polyfills.ts deleted file mode 100644 index 4255c0a44..000000000 --- a/packages/signals/signals/src/core/processor/polyfills.ts +++ /dev/null @@ -1,16 +0,0 @@ -const globalThisPolyfill = `(function () { - // polyfill for globalThis - if (typeof globalThis === 'undefined') { - if (typeof self !== 'undefined') { - self.globalThis = self - } else if (typeof window !== 'undefined') { - window.globalThis = window - } else if (typeof global !== 'undefined') { - global.globalThis = global - } else { - throw new Error('Unable to locate global object') - } - } -})()` - -export const polyfills = [globalThisPolyfill].join('\n') diff --git a/packages/signals/signals/src/core/processor/processor.ts b/packages/signals/signals/src/core/processor/processor.ts deleted file mode 100644 index 6f4d567f2..000000000 --- a/packages/signals/signals/src/core/processor/processor.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { logger } from '../../lib/logger' -import { Signal } from '@segment/analytics-signals-runtime' -import { AnyAnalytics } from '../../types' -import { AnalyticsMethodCalls, MethodName, SignalSandbox } from './sandbox' - -export class SignalEventProcessor { - analytics: AnyAnalytics - sandbox: SignalSandbox - constructor(analytics: AnyAnalytics, sandbox: SignalSandbox) { - this.analytics = analytics - this.sandbox = sandbox - } - - async process(signal: Signal, signals: Signal[]) { - let analyticsMethodCalls: AnalyticsMethodCalls | undefined - try { - analyticsMethodCalls = await this.sandbox.execute(signal, signals) - } catch (err) { - // in practice, we should never hit this error, but if we do, we should log it. - console.error('Error processing signal', { signal, signals }, err) - return - } - - for (const methodName in analyticsMethodCalls) { - const name = methodName as MethodName - const eventsCollection = analyticsMethodCalls[name] - eventsCollection.forEach((args) => { - logger.info('New method call:', `analytics.${name}()`, args) - - // @ts-ignore - this.analytics[name](...args) - }) - } - } - - cleanup() { - return this.sandbox.destroy() - } -} diff --git a/packages/signals/signals/src/core/processor/sandbox.ts b/packages/signals/signals/src/core/processor/sandbox.ts deleted file mode 100644 index 957790cbf..000000000 --- a/packages/signals/signals/src/core/processor/sandbox.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { logger } from '../../lib/logger' -import { createWorkerBox, WorkerBoxAPI } from '../../lib/workerbox' -import { resolvers } from './arg-resolvers' -import { AnalyticsRuntimePublicApi, ProcessSignal } from '../../types' -import { replaceBaseUrl } from '../../lib/replace-base-url' -import { Signal, WebSignalsRuntime } from '@segment/analytics-signals-runtime' -import { getRuntimeCode } from '@segment/analytics-signals-runtime' -import { polyfills } from './polyfills' -import { loadScript } from '../../lib/load-script' - -export type MethodName = - | 'page' - | 'identify' - | 'track' - | 'alias' - | 'screen' - | 'group' - -/** - * Buffer of any analytics calls made during the processing of a signal - */ -export type AnalyticsMethodCalls = Record & { - reset: unknown[] -} - -/** - * Proxy around the analytics client - */ -class AnalyticsRuntime implements AnalyticsRuntimePublicApi { - private calls: AnalyticsMethodCalls = { - page: [], - identify: [], - track: [], - alias: [], - screen: [], - group: [], - reset: [], - } - - getCalls(): AnalyticsMethodCalls { - return this.calls - } - - /** - * Stamp the context with the event origin to prevent infinite signal-event loops. - */ - private stamp(options: Record): Record { - if (!options) { - options = {} - } - options.context = { ...options.context, __eventOrigin: { type: 'Signal' } } - return options - } - - // these methods need to be bound to the instance, rather than the prototype, in order to serialize correctly in the sandbox. - track = (...args: any[]) => { - try { - // @ts-ignore - const [eventName, props, options, cb] = resolvers.resolveArguments( - // @ts-ignore - ...args - ) - this.calls.track.push([eventName, props, this.stamp(options), cb]) - } catch (err) { - // wrapping all methods in a try/catch because throwing an error won't cause the error to surface inside of workerboxjs - console.error(err) - } - } - - identify = (...args: any[]) => { - try { - // @ts-ignore - const [id, traits, options, cb] = resolvers.resolveUserArguments(...args) - this.calls.identify.push([id, traits, this.stamp(options), cb]) - } catch (err) { - console.error(err) - } - } - - alias = (...args: any[]) => { - try { - const [userId, previousId, options, cb] = resolvers.resolveAliasArguments( - // @ts-ignore - ...args - ) - this.calls.alias.push([userId, previousId, this.stamp(options), cb]) - } catch (err) { - console.error(err) - } - } - group = (...args: any[]) => { - try { - // @ts-ignore - const [id, traits, options, cb] = resolvers.resolveUserArguments(...args) - this.calls.group.push([id, traits, this.stamp(options), cb]) - } catch (err) { - console.error(err) - } - } - - page = (...args: any[]) => { - try { - const [category, name, props, options, cb] = - resolvers.resolvePageArguments(...args) - this.stamp(options) - this.calls.page.push([category, name, props, this.stamp(options), cb]) - } catch (err) { - console.error(err) - } - } - - screen = (...args: any[]) => { - try { - const [category, name, props, options, cb] = - resolvers.resolvePageArguments(...args) - this.stamp(options) - this.calls.screen.push([category, name, props, this.stamp(options), cb]) - } catch (err) { - console.error(err) - } - } - - reset = () => { - this.calls.reset.push([]) - } -} - -interface CodeSandbox { - run: (fn: string, scope: Record) => Promise - destroy: () => Promise -} - -class JavascriptSandbox implements CodeSandbox { - private workerbox: Promise - constructor() { - this.workerbox = createWorkerBox() - } - async run(fn: string, scope: Record) { - try { - const wb = await this.workerbox - await wb.run(fn, scope) - } catch (err) { - console.error('processSignal() error in sandbox', err, { - fn, - }) - } - } - - async destroy(): Promise { - const wb = await this.workerbox - await wb.destroy() - } -} - -export const normalizeEdgeFunctionURL = ( - functionHost: string | undefined, - edgeFnDownloadURL: string | undefined -) => { - if (functionHost && edgeFnDownloadURL) { - replaceBaseUrl(edgeFnDownloadURL, `https://${functionHost}`) - } else { - return edgeFnDownloadURL - } -} - -export type SandboxSettingsConfig = { - functionHost: string | undefined - processSignal: string | undefined - edgeFnDownloadURL: string | undefined - edgeFnFetchClient?: typeof fetch - sandboxStrategy: 'iframe' | 'global' -} - -export type IframeSandboxSettingsConfig = Pick< - SandboxSettingsConfig, - 'processSignal' | 'edgeFnFetchClient' | 'edgeFnDownloadURL' -> - -const consoleWarnProcessSignal = () => - console.warn( - 'processSignal is not defined - have you set up auto-instrumentation on app.segment.com?' - ) - -export class IframeSandboxSettings { - /** - * Should look like: - * ```js - * function processSignal(signal) { - * ... - * } - * ``` - */ - processSignal: Promise - constructor(settings: IframeSandboxSettingsConfig) { - const fetch = settings.edgeFnFetchClient ?? globalThis.fetch - - let processSignalNormalized = Promise.resolve( - `globalThis.processSignal = function() {}` - ) - - if (settings.processSignal) { - processSignalNormalized = Promise.resolve(settings.processSignal).then( - (str) => `globalThis.processSignal = ${str}` - ) - } else if (settings.edgeFnDownloadURL) { - processSignalNormalized = fetch(settings.edgeFnDownloadURL!).then((res) => - res.text() - ) - } else { - consoleWarnProcessSignal() - } - - this.processSignal = processSignalNormalized - } -} - -export interface SignalSandbox { - execute( - signal: Signal, - signals: Signal[] - ): Promise - destroy(): void | Promise -} - -export class WorkerSandbox implements SignalSandbox { - settings: IframeSandboxSettings - jsSandbox: CodeSandbox - - constructor(settings: IframeSandboxSettings) { - this.settings = settings - this.jsSandbox = new JavascriptSandbox() - } - - async execute( - signal: Signal, - signals: Signal[] - ): Promise { - const analytics = new AnalyticsRuntime() - const scope = { - analytics, - } - logger.debug('processing signal', { signal, scope, signals }) - const code = [ - polyfills, - await this.settings.processSignal, - getRuntimeCode(), - `signals.signalBuffer = ${JSON.stringify(signals)};`, - 'try { processSignal(' + - JSON.stringify(signal) + - ', { analytics, signals }); } catch(err) { console.error("Process signal failed.", err); }', - ].join('\n') - await this.jsSandbox.run(code, scope) - - const calls = analytics.getCalls() - return calls - } - destroy(): void { - void this.jsSandbox.destroy() - } -} - -// ProcessSignal unfortunately uses globals. This should change. -// For now, we are setting up the globals between each invocation -const processWithGlobalScopeExecutionEnv = ( - signal: Signal, - signalBuffer: Signal[] -): AnalyticsMethodCalls | undefined => { - const g = globalThis as any - const processSignal: ProcessSignal = g['processSignal'] - - if (typeof processSignal == 'undefined') { - consoleWarnProcessSignal() - return undefined - } - - // processSignal expects a global called `signals` -- of course, there can local variable naming conflict on the client, which is why globals were a bad idea. - const analytics = new AnalyticsRuntime() - const signals = new WebSignalsRuntime(signalBuffer) - - const originalAnalytics = g.analytics - if (originalAnalytics instanceof AnalyticsRuntime) { - throw new Error( - 'Invariant: analytics variable was not properly restored on the previous execution. This indicates a concurrency bug' - ) - } - const originalSignals = g.signals - - try { - g['analytics'] = analytics - g['signals'] = signals - processSignal(signal, { - // we eventually want to get rid of globals and processSignal just uses local variables. - // TODO: update processSignal generator to accept params like these for web (mobile currently uses globals for their architecture -- can be changed but hard). - analytics: analytics, - signals: signals, - // constants - }) - } finally { - // restore globals - g['analytics'] = originalAnalytics - g['signals'] = originalSignals - } - - return analytics.getCalls() -} - -/** - * Sandbox that avoids CSP errors, but evaluates everything globally - */ -interface GlobalScopeSandboxSettings { - edgeFnDownloadURL: string -} -export class GlobalScopeSandbox implements SignalSandbox { - htmlScriptLoaded: Promise - - constructor(settings: GlobalScopeSandboxSettings) { - logger.debug('Initializing global scope sandbox') - this.htmlScriptLoaded = loadScript(settings.edgeFnDownloadURL) - } - - async execute(signal: Signal, signals: Signal[]) { - await this.htmlScriptLoaded - return processWithGlobalScopeExecutionEnv(signal, signals) - } - destroy(): void {} -} - -export class NoopSandbox implements SignalSandbox { - execute(_signal: Signal, _signals: Signal[]) { - return Promise.resolve(undefined) - } - destroy(): void {} -} diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/change-gen.test.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/change-gen.test.ts deleted file mode 100644 index d6f74e9ce..000000000 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/change-gen.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { SignalEmitter } from '../../../emitter' -import { OnChangeGenerator } from '../change-gen' - -describe(OnChangeGenerator, () => { - let onChangeGenerator: OnChangeGenerator - let emitter: SignalEmitter - let unregister: () => void - beforeEach(async () => { - onChangeGenerator = new OnChangeGenerator() - emitter = new SignalEmitter() - await emitter.start({} as any) - }) - - afterEach(() => { - unregister() - }) - - it('should emit a signal on change event', async () => { - const emitSpy = jest.spyOn(emitter, 'emit') - const target = document.createElement('input') - target.type = 'text' - target.value = 'new value' - - const event = new Event('change', { bubbles: true }) - Object.defineProperty(event, 'target', { value: target }) - - unregister = onChangeGenerator.register(emitter) - document.dispatchEvent(event) - - expect(emitSpy.mock.calls.length).toBe(1) - expect(emitSpy.mock.calls[0][0]).toMatchInlineSnapshot(` - { - "anonymousId": "", - "context": { - "library": { - "name": "@segment/analytics-next", - "version": "0.0.0", - }, - "signalsRuntime": "", - }, - "data": { - "change": { - "value": "new value", - }, - "eventType": "change", - "listener": "onchange", - "page": { - "hash": "", - "hostname": "localhost", - "path": "/", - "referrer": "", - "search": "", - "title": "", - "url": "http://localhost/", - }, - "target": { - "attributes": { - "type": "text", - }, - "checked": false, - "classList": [], - "describedBy": undefined, - "id": "", - "innerText": undefined, - "label": undefined, - "labels": [], - "name": "", - "nodeName": "INPUT", - "tagName": "INPUT", - "textContent": "", - "title": "", - "type": "text", - "value": "new value", - }, - }, - "index": undefined, - "timestamp": , - "type": "interaction", - } - `) - }) - - it('should not emit a signal for ignored elements', () => { - const emitSpy = jest.spyOn(emitter, 'emit') - const target = document.createElement('input') - target.type = 'password' - - const event = new Event('change', { bubbles: true }) - Object.defineProperty(event, 'target', { value: target }) - - unregister = onChangeGenerator.register(emitter) - document.dispatchEvent(event) - - expect(emitSpy).not.toHaveBeenCalled() - }) - - it('should not emit a signal for elements handled by mutation observer', () => { - const emitSpy = jest.spyOn(emitter, 'emit') - const target = document.createElement('input') - target.type = 'text' - target.setAttribute('value', 'initial value') - - const event = new Event('change', { bubbles: true }) - Object.defineProperty(event, 'target', { value: target }) - - unregister = onChangeGenerator.register(emitter) - document.dispatchEvent(event) - - expect(emitSpy).not.toHaveBeenCalled() - }) - - it('should emit a signal with selectedOptions for select elements', () => { - const emitSpy = jest.spyOn(emitter, 'emit') - const target = document.createElement('select') - const option1 = document.createElement('option') - option1.value = 'value1' - option1.label = 'label1' - option1.selected = true - const option2 = document.createElement('option') - option2.value = 'value2' - option2.label = 'label2' - target.append(option1, option2) - - const event = new Event('change', { bubbles: true }) - Object.defineProperty(event, 'target', { value: target }) - - unregister = onChangeGenerator.register(emitter) - document.dispatchEvent(event) - - expect(emitSpy.mock.lastCall).toMatchInlineSnapshot(` - [ - { - "anonymousId": "", - "context": { - "library": { - "name": "@segment/analytics-next", - "version": "0.0.0", - }, - "signalsRuntime": "", - }, - "data": { - "change": { - "selectedOptions": [ - { - "label": "label1", - "value": "value1", - }, - ], - }, - "eventType": "change", - "listener": "onchange", - "page": { - "hash": "", - "hostname": "localhost", - "path": "/", - "referrer": "", - "search": "", - "title": "", - "url": "http://localhost/", - }, - "target": { - "attributes": {}, - "classList": [], - "describedBy": undefined, - "id": "", - "innerText": undefined, - "label": undefined, - "labels": [], - "name": "", - "nodeName": "SELECT", - "selectedIndex": 0, - "selectedOptions": [ - { - "label": "label1", - "value": "value1", - }, - ], - "tagName": "SELECT", - "textContent": "", - "title": "", - "type": "select-one", - "value": "value1", - }, - }, - "index": undefined, - "timestamp": , - "type": "interaction", - }, - ] - `) - }) -}) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/clean-text.test.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/clean-text.test.ts deleted file mode 100644 index 5e7f42a9c..000000000 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/clean-text.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { cleanText } from '../helpers' - -describe(cleanText, () => { - test('should remove newline characters', () => { - const input = 'Hello\nWorld\n' - const expected = 'Hello World' - expect(cleanText(input)).toBe(expected) - }) - - test('should remove tab characters', () => { - const input = 'Hello\tWorld\t' - const expected = 'Hello World' - expect(cleanText(input)).toBe(expected) - }) - - test('should replace multiple spaces with a single space', () => { - const input = 'Hello World' - const expected = 'Hello World' - expect(cleanText(input)).toBe(expected) - }) - - test('should replace non-breaking spaces with regular spaces', () => { - const input = 'Hello\u00A0World' - const expected = 'Hello World' - expect(cleanText(input)).toBe(expected) - }) - - test('should trim leading and trailing spaces', () => { - const input = ' Hello World ' - const expected = 'Hello World' - expect(cleanText(input)).toBe(expected) - }) - - test('should handle a combination of special characters', () => { - const input = ' \n\tHello\u00A0 World\n\t ' - const expected = 'Hello World' - expect(cleanText(input)).toBe(expected) - }) - - test('should return an empty string if input is empty', () => { - const input = '' - const expected = '' - expect(cleanText(input)).toBe(expected) - }) - - test('should return the same string if there are no special characters', () => { - const input = 'Hello World' - const expected = 'Hello World' - expect(cleanText(input)).toBe(expected) - }) -}) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/element-parser.test.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/element-parser.test.ts deleted file mode 100644 index 59583549c..000000000 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/element-parser.test.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { parseElement } from '../element-parser' - -describe(parseElement, () => { - test('parses a generic HTML element', () => { - const element = document.createElement('div') - element.id = 'test-id' - element.classList.add('test-class') - element.setAttribute('title', 'Test Title') - element.textContent = 'Test Content' - - const parsed = parseElement(element) - - expect(parsed).toMatchInlineSnapshot(` - { - "attributes": { - "class": "test-class", - "id": "test-id", - "title": "Test Title", - }, - "classList": [ - "test-class", - ], - "describedBy": undefined, - "id": "test-id", - "innerText": undefined, - "label": undefined, - "labels": [], - "name": undefined, - "nodeName": "DIV", - "tagName": "DIV", - "textContent": "Test Content", - "title": "Test Title", - "type": undefined, - "value": undefined, - } - `) - }) - - test('parses an HTMLSelectElement', () => { - const select = document.createElement('select') - select.id = 'select-id' - select.classList.add('select-class') - select.setAttribute('title', 'Select Title') - select.selectedIndex = 1 - - const option1 = document.createElement('option') - option1.value = 'value1' - option1.label = 'label1' - select.appendChild(option1) - - const option2 = document.createElement('option') - option2.value = 'value2' - option2.label = 'label2' - select.appendChild(option2) - - select.selectedIndex = 1 - - const parsed = parseElement(select) - - expect(parsed).toMatchInlineSnapshot(` - { - "attributes": { - "class": "select-class", - "id": "select-id", - "title": "Select Title", - }, - "classList": [ - "select-class", - ], - "describedBy": undefined, - "id": "select-id", - "innerText": undefined, - "label": undefined, - "labels": [], - "name": "", - "nodeName": "SELECT", - "selectedIndex": 1, - "selectedOptions": [ - { - "label": "label2", - "value": "value2", - }, - ], - "tagName": "SELECT", - "textContent": "", - "title": "Select Title", - "type": "select-one", - "value": "value2", - } - `) - }) - - test('parses an HTMLInputElement', () => { - const input = document.createElement('input') - input.id = 'input-id' - input.classList.add('input-class') - input.setAttribute('title', 'Input Title') - input.type = 'checkbox' - input.checked = true - - const parsed = parseElement(input) - - expect(parsed).toMatchInlineSnapshot(` - { - "attributes": { - "class": "input-class", - "id": "input-id", - "title": "Input Title", - "type": "checkbox", - }, - "checked": true, - "classList": [ - "input-class", - ], - "describedBy": undefined, - "id": "input-id", - "innerText": undefined, - "label": undefined, - "labels": [], - "name": "", - "nodeName": "INPUT", - "tagName": "INPUT", - "textContent": "", - "title": "Input Title", - "type": "checkbox", - "value": "on", - } - `) - }) - - test('parses an HTMLMediaElement', () => { - const video = document.createElement('video') - video.id = 'video-id' - video.classList.add('video-class') - video.setAttribute('title', 'Video Title') - video.src = 'video.mp4' - video.currentTime = 10 - - // Mock the duration property - Object.defineProperty(video, 'duration', { - value: 120, - writable: true, - }) - - Object.defineProperty(video, 'paused', { - value: false, - writable: true, - }) - - Object.defineProperty(video, 'readyState', { - value: 4, - writable: true, - }) - - video.muted = true - video.playbackRate = 1.5 - video.volume = 0.8 - - const parsed = parseElement(video) - - expect(parsed).toMatchInlineSnapshot(` - { - "attributes": { - "class": "video-class", - "id": "video-id", - "src": "video.mp4", - "title": "Video Title", - }, - "classList": [ - "video-class", - ], - "currentSrc": "", - "currentTime": 10, - "describedBy": undefined, - "duration": 120, - "ended": false, - "id": "video-id", - "innerText": undefined, - "label": undefined, - "labels": [], - "muted": true, - "name": undefined, - "nodeName": "VIDEO", - "paused": false, - "playbackRate": 1.5, - "readyState": 4, - "src": "http://localhost/video.mp4", - "tagName": "VIDEO", - "textContent": "", - "title": "Video Title", - "type": undefined, - "value": undefined, - "volume": 0.8, - } - `) - }) - - test('parses an HTMLFormElement', () => { - const form = document.createElement('input') - form.id = 'form-id' - form.classList.add('form-class') - form.setAttribute('title', 'Form Title') - - const input = document.createElement('input') - input.name = 'input-name' - input.value = 'input-value' - form.appendChild(input) - - const parsed = parseElement(form) - - expect(parsed).toMatchInlineSnapshot(` - { - "attributes": { - "class": "form-class", - "id": "form-id", - "title": "Form Title", - }, - "checked": false, - "classList": [ - "form-class", - ], - "describedBy": undefined, - "id": "form-id", - "innerText": undefined, - "label": undefined, - "labels": [], - "name": "", - "nodeName": "INPUT", - "tagName": "INPUT", - "textContent": "", - "title": "Form Title", - "type": "text", - "value": "", - } - `) - }) - test('handles scenarios where name is an object', () => { - const form = document.createElement('form') - form.id = 'form-id' - form.classList.add('form-class') - form.setAttribute('title', 'Form Title') - - Object.defineProperty(form, 'name', { - // this can happen in some weird cases?. just in case - value: { - hello: 'world', - }, - writable: true, - }) - - const parsed = parseElement(form) - - expect(parsed).toMatchInlineSnapshot(` - { - "attributes": { - "class": "form-class", - "id": "form-id", - "title": "Form Title", - }, - "classList": [ - "form-class", - ], - "describedBy": undefined, - "formData": {}, - "id": "form-id", - "innerText": undefined, - "label": undefined, - "labels": [], - "name": undefined, - "nodeName": "FORM", - "tagName": "FORM", - "textContent": undefined, - "title": "Form Title", - "type": undefined, - "value": undefined, - } - `) - }) -}) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts deleted file mode 100644 index 6cf8a36a4..000000000 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/mutation-observer.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -/* eslint-disable jest/no-done-callback */ -import { sleep } from '@segment/analytics-core' -import { - MutationObservable, - MutationObservableSettings, - MutationObservableSubscriber, -} from '../mutation-observer' - -describe('MutationObservable', () => { - let mutationObservable: MutationObservable - let testButton: HTMLButtonElement - let testInput: HTMLInputElement - const subscribeFn = jest.fn() as jest.Mock - beforeEach(() => { - document.body.innerHTML = - '
' + - '' - testButton = document.getElementById('test-element') as HTMLButtonElement - testInput = document.getElementById('test-input') as HTMLInputElement - }) - - afterEach(() => { - mutationObservable.cleanup() - }) - - it('should capture attribute changes', async () => { - mutationObservable = new MutationObservable( - new MutationObservableSettings({ - observedRoles: () => ['button'], - observedAttributes: () => ['aria-pressed'], - debounceMs: 500, - }) - ) - - mutationObservable.subscribe(subscribeFn) - testButton.setAttribute('aria-pressed', 'true') - await sleep(0) - - expect(subscribeFn).toHaveBeenCalledTimes(1) - expect(subscribeFn).toHaveBeenCalledWith({ - element: testButton, - attributes: { 'aria-pressed': 'true' }, - }) - }) - - it('should capture multiple attribute changes', async () => { - mutationObservable = new MutationObservable( - new MutationObservableSettings({ - observedRoles: () => ['button'], - observedAttributes: () => ['aria-pressed'], - debounceMs: 500, - }) - ) - - mutationObservable.subscribe(subscribeFn) - testButton.setAttribute('aria-pressed', 'true') - await sleep(0) - testButton.setAttribute('aria-pressed', 'false') - await sleep(0) - - expect(subscribeFn).toHaveBeenCalledTimes(2) - expect(subscribeFn).toHaveBeenNthCalledWith(1, { - element: testButton, - attributes: { 'aria-pressed': 'true' }, - }) - expect(subscribeFn).toHaveBeenNthCalledWith(2, { - element: testButton, - attributes: { 'aria-pressed': 'false' }, - }) - }) - - it('should debounce text inputs', async () => { - mutationObservable = new MutationObservable( - new MutationObservableSettings({ - debounceMs: 100, - }) - ) - mutationObservable.subscribe(subscribeFn) - testInput.setAttribute('value', 'hello') - await sleep(0) - testInput.setAttribute('value', 'hello wor') - await sleep(0) - testInput.setAttribute('value', 'hello world') - - await sleep(200) - expect(subscribeFn).toHaveBeenCalledTimes(1) - expect(subscribeFn).toHaveBeenCalledWith({ - element: testInput, - attributes: { value: 'hello world' }, - }) - }) - it('should debounce tect inputs if happening in the same tick', async () => { - mutationObservable = new MutationObservable( - new MutationObservableSettings({ - debounceMs: 50, - }) - ) - mutationObservable.subscribe(subscribeFn) - testInput.setAttribute('value', 'hello') - testInput.setAttribute('value', 'hello wor') - testInput.setAttribute('value', 'hello world') - await sleep(100) - - expect(subscribeFn).toHaveBeenCalledTimes(1) - expect(subscribeFn).toHaveBeenCalledWith({ - element: testInput, - attributes: { value: 'hello world' }, - }) - }) - - it('should handle multiple attributes changing', async () => { - mutationObservable = new MutationObservable( - new MutationObservableSettings({ - debounceMs: 100, - observedAttributes: (roles) => [...roles, 'aria-foo'], - }) - ) - mutationObservable.subscribe(subscribeFn) - testInput.setAttribute('value', 'hello') - testInput.setAttribute('aria-foo', 'bar') - await sleep(200) - - expect(subscribeFn).toHaveBeenCalledTimes(1) - expect(subscribeFn).toHaveBeenCalledWith({ - element: testInput, - attributes: { value: 'hello', 'aria-foo': 'bar' }, - }) - }) - - it('should not emit duplicate events', async () => { - mutationObservable = new MutationObservable( - new MutationObservableSettings({ - observedRoles: () => ['button'], - observedAttributes: () => ['aria-pressed'], - debounceMs: 0, - }) - ) - - mutationObservable.subscribe(subscribeFn) - testButton.setAttribute('aria-pressed', 'true') - await sleep(0) - testButton.setAttribute('aria-pressed', 'true') - await sleep(0) - - expect(subscribeFn).toHaveBeenCalledTimes(1) - expect(subscribeFn).toHaveBeenCalledWith({ - element: testButton, - attributes: { 'aria-pressed': 'true' }, - }) - }) - - it('should not emit duplicate events if overlapping', async () => { - mutationObservable = new MutationObservable( - new MutationObservableSettings({ - observedRoles: () => ['button'], - observedAttributes: () => ['aria-pressed', 'aria-foo'], - debounceMs: 0, - }) - ) - - mutationObservable.subscribe(subscribeFn) - testButton.setAttribute('aria-pressed', 'true') - testButton.setAttribute('aria-foo', 'bar') - await sleep(0) - - testButton.setAttribute('aria-pressed', 'false') - await sleep(50) - - testButton.setAttribute('aria-pressed', 'false') - await sleep(50) - - testButton.setAttribute('aria-foo', 'bar') - await sleep(50) - - expect(subscribeFn).toHaveBeenNthCalledWith(1, { - element: testButton, - attributes: { 'aria-pressed': 'true', 'aria-foo': 'bar' }, - }) - - expect(subscribeFn).toHaveBeenNthCalledWith(2, { - element: testButton, - attributes: { 'aria-pressed': 'false' }, - }) - expect(subscribeFn).toHaveBeenCalledTimes(2) - }) - - it('should not emit event for aria-selected=false', (done) => { - mutationObservable = new MutationObservable( - new MutationObservableSettings({ - observedRoles: () => ['button'], - observedAttributes: () => ['aria-selected'], - }) - ) - - mutationObservable.subscribe(() => { - done.fail('Should not emit event for aria-selected=false') - }) - - testButton.setAttribute('aria-selected', 'false') - setTimeout(done, 1000) // Wait to ensure no event is emitted - }) -}) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/navigation-gen.test.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/navigation-gen.test.ts deleted file mode 100644 index d29066128..000000000 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/__tests__/navigation-gen.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { jest } from '@jest/globals' -import { URLChangeNavigationData } from '@segment/analytics-signals-runtime' -import { setLocation } from '../../../../test-helpers/set-location' -import { SignalEmitter } from '../../../emitter' -import { OnNavigationEventGenerator } from '../navigation-gen' - -const originalLocation = window.location - -describe(OnNavigationEventGenerator, () => { - let emitter: SignalEmitter - let emitSpy: jest.SpiedFunction - - beforeEach(() => { - setLocation(originalLocation) - jest.useFakeTimers() - emitter = new SignalEmitter() - emitSpy = jest.spyOn(emitter, 'emit') - }) - - afterEach(() => { - jest.clearAllMocks() - }) - - it('should emit an event with action "pageLoad" on initialization', () => { - const generator = new OnNavigationEventGenerator() - generator.register(emitter) - expect(emitSpy).toHaveBeenCalledTimes(1) - expect(emitSpy.mock.lastCall).toMatchInlineSnapshot(` - [ - { - "anonymousId": "", - "context": { - "library": { - "name": "@segment/analytics-next", - "version": "0.0.0", - }, - "signalsRuntime": "", - }, - "data": { - "currentUrl": "http://localhost/", - "hash": "", - "page": { - "hash": "", - "hostname": "localhost", - "path": "/", - "referrer": "", - "search": "", - "title": "", - "url": "http://localhost/", - }, - "path": "/", - "search": "", - "title": "", - }, - "index": undefined, - "timestamp": , - "type": "navigation", - }, - ] - `) - }) - - it('should emit an event with "action: urlChange" when the URL changes', () => { - const generator = new OnNavigationEventGenerator() - - generator.register(emitter) - - // Simulate a URL change - const newUrl = new URL(location.href) - newUrl.pathname = '/new-path' - newUrl.search = '?query=123' - newUrl.hash = '#hello' - setLocation({ - href: newUrl.href, - pathname: newUrl.pathname, - search: newUrl.search, - hash: newUrl.hash, - }) - - // Advance the timers to trigger the polling - jest.advanceTimersByTime(1000) - - expect(emitSpy).toHaveBeenCalledTimes(2) - - expect(emitSpy.mock.lastCall).toMatchInlineSnapshot(` - [ - { - "anonymousId": "", - "context": { - "library": { - "name": "@segment/analytics-next", - "version": "0.0.0", - }, - "signalsRuntime": "", - }, - "data": { - "changedProperties": [ - "path", - "search", - "hash", - ], - "currentUrl": "http://localhost/new-path?query=123#hello", - "hash": "#hello", - "page": { - "hash": "#hello", - "hostname": "localhost", - "path": "/new-path", - "referrer": "", - "search": "?query=123", - "title": "", - "url": "http://localhost/new-path?query=123#hello", - }, - "path": "/new-path", - "previousUrl": "http://localhost/", - "search": "?query=123", - "title": "", - }, - "index": undefined, - "timestamp": , - "type": "navigation", - }, - ] - `) - }) - - it('should only list the property that actually changed in changedProperties, and no more/less', () => { - const generator = new OnNavigationEventGenerator() - - generator.register(emitter) - - const newUrl = new URL(location.href) - newUrl.hash = '#hello' - setLocation({ - href: newUrl.href, - hash: newUrl.hash, - }) - - jest.advanceTimersByTime(1000) - const lastCall = emitSpy.mock.lastCall![0].data as URLChangeNavigationData - expect(lastCall.changedProperties).toEqual(['hash']) - }) - - it('should stop emitting events after unsubscribe is called', () => { - const generator = new OnNavigationEventGenerator() - - const unsubscribe = generator.register(emitter) - - // Simulate a URL change - const newUrl = new URL(location.href) - newUrl.pathname = '/new-path' - setLocation({ - href: newUrl.href, - pathname: newUrl.pathname, - }) - - // Advance the timers to trigger the polling - jest.advanceTimersByTime(1000) - - // Ensure the event is emitted before unsubscribe - expect(emitSpy).toHaveBeenCalledTimes(2) - - // Unsubscribe the generator - unsubscribe() - - // Simulate another URL change - newUrl.pathname = '/another-path' - setLocation({ - href: newUrl.href, - pathname: newUrl.pathname, - }) - - // Advance the timers again - jest.advanceTimersByTime(1000) - - // Ensure no additional events are emitted after unsubscribe - expect(emitSpy).toHaveBeenCalledTimes(2) - }) -}) diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts deleted file mode 100644 index b26e5bfb1..000000000 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/change-gen.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { logger } from '../../../lib/logger' -import { createInteractionSignal } from '../../../types/factories' -import { SignalEmitter } from '../../emitter' -import { SignalGlobalSettings } from '../../signals' -import { SignalGenerator } from '../types' -import { shouldIgnoreElement } from './dom-gen' -import { parseElement } from './element-parser' -import { - MutationObservable, - AttributeChangedEvent, - MutationObservableSettings, -} from './mutation-observer' - -export class MutationGeneratorSettings extends MutationObservableSettings {} - -export class MutationChangeGenerator implements SignalGenerator { - id = 'mutation' - private elMutObserver: MutationObservable - /** - * Custom selectors that should be ignored by the mutation observer - * e.g if you have a custom input field that is not a standard input field, you can add it here - */ - customSelectors = [] - constructor(settings: SignalGlobalSettings) { - this.elMutObserver = new MutationObservable(settings.mutationGenerator) - } - - register(emitter: SignalEmitter) { - const callback = (ev: AttributeChangedEvent) => { - const target = ev.element as HTMLElement | null - if (!target || shouldIgnoreElement(target)) { - return - } - const el = parseElement(ev.element) - emitter.emit( - createInteractionSignal({ - eventType: 'change', - target: el, - listener: 'mutation', - change: ev.attributes, - }) - ) - } - this.elMutObserver.subscribe(callback) - return () => this.elMutObserver.cleanup() - } -} - -export class OnChangeGenerator implements SignalGenerator { - id = 'change' - - register(emitter: SignalEmitter) { - /** - * Magic attributes that we use to normalize the API between the mutation listener - * and the onchange listener - */ - type ChangedEvent = { - checked?: boolean - value?: string - files?: string[] - selectedOptions?: { label: string; value: string }[] - } - - /** - * Extract the change from a change event for stateless elistener lements, - * so we can normalize the response between mutation listener changes and onchange listener events - */ - const parseChange = (target: HTMLElement): ChangedEvent | undefined => { - if (target instanceof HTMLSelectElement) { - return { - selectedOptions: Array.from(target.selectedOptions).map((option) => ({ - value: option.value, - label: option.label, - })), - } - } - if (target instanceof HTMLTextAreaElement) { - return { value: target.value } - } - if (target instanceof HTMLInputElement) { - if ('value' in target || 'checked' in target) { - if (target.type === 'checkbox' || target.type === 'radio') { - return { checked: target.checked } - } - if (target.type === 'file') { - return { - files: Array.from(target.files ?? []).map((f) => f.name), - } - } - return { value: target.value } - } - } - } - const isHandledByMutationObserver = (el: HTMLElement): boolean => { - // check if the element is stateful -- if it is, we should ignore the onchange event since the mutation observer will pick it up - // input fields where can modify the field through interactions: - const inputTypesWithMutableValue = [ - 'text', - 'password', - 'email', - 'url', - 'tel', - 'number', - 'search', - 'date', - 'time', - 'datetime-local', - 'month', - 'week', - 'color', - 'range', - ] - const type = el.getAttribute('type') - const isInput = el instanceof HTMLInputElement - if ( - isInput && - (type === null || inputTypesWithMutableValue.includes(type)) - ) { - return el.getAttribute('value') !== null - } - return false - } - - // vanilla change events do not trigger dom updates. - const handleOnChangeEvent = (ev: Event) => { - const target = ev.target as HTMLElement | null - if (!target || shouldIgnoreElement(target)) { - return - } - // if the element is an input with a value, we can use mutation observer to get the new value, so we don't send duplicate signals - // this can really only happen with inputs, so we don't need to check for other elements - // This is very hacky -- onChange has different semantics than the value mutation (onchange event only fires when the element loses focus), so it's not a perfect match. - // We're not sure what the tolerance for duplicate-ish signals is since we have both strategies available? - if (isHandledByMutationObserver(target)) { - logger.debug('Ignoring onchange event in stateful element', target) - return - } - - const el = parseElement(target) - const change = parseChange(target) - if (!change) { - logger.debug( - 'No change found on element..., this should not happen', - el - ) - return - } - emitter.emit( - createInteractionSignal({ - eventType: 'change', - listener: 'onchange', - target: el, - change, - }) - ) - } - - document.addEventListener('change', handleOnChangeEvent, true) - return () => { - document.removeEventListener('change', handleOnChangeEvent, true) - } - } -} - -export class ContentEditableChangeGenerator implements SignalGenerator { - id = 'contenteditable' - register(emitter: SignalEmitter) { - const commitChange = (ev: Event) => { - if (!(ev.target instanceof HTMLElement)) { - return - } - const target = ev.target as HTMLElement - const el = parseElement(target) - emitter.emit( - createInteractionSignal({ - eventType: 'change', - listener: 'contenteditable', - target: el, - change: { - textContent: el.textContent || null, - }, - }) - ) - } - - const handleContentEditableChange = (ev: Event) => { - const target = ev.target as HTMLElement | null - const editable = target instanceof HTMLElement && target.isContentEditable - if (!editable) { - return - } - - // normalize so this behaves like a change event on an input field -- so it doesn't fire on every keystroke. - target.addEventListener('blur', commitChange, { once: true }) - } - document.addEventListener('input', handleContentEditableChange, true) - - return () => - document.removeEventListener('input', handleContentEditableChange) - } -} diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/dom-gen.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/dom-gen.ts deleted file mode 100644 index 5f20c2594..000000000 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/dom-gen.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { createInteractionSignal } from '../../../types/factories' -import { SignalEmitter } from '../../emitter' -import { SignalGenerator } from '../types' -import { parseElement } from './element-parser' - -export class ClickSignalsGenerator implements SignalGenerator { - id = 'click' - - register(emitter: SignalEmitter) { - const handleClick = (ev: MouseEvent) => { - const target = ev.target as HTMLElement | null - if (!target) return - const el = this.getClosestClickableElement(target) - if (el) { - emitter.emit( - createInteractionSignal({ - eventType: 'click', - target: parseElement(el), - }) - ) - } - } - document.addEventListener('click', handleClick, true) - return () => document.removeEventListener('click', handleClick) - } - - private getClosestClickableElement(el: HTMLElement): HTMLElement | null { - // if you click on a nested element, we want to get the closest clickable ancestor. Useful for things like buttons with nested value or images - const selector = [ - 'button', - 'a', - 'option', - '[role="button"]', - '[role="link"]', - '[role="menuitem"]', - '[role="menuitemcheckbox"]', - '[role="menuitemradio"]', - '[role="tab"]', - '[role="option"]', - '[role="switch"]', - '[role="treeitem"]', - ].join(', ') - return el.closest(selector) - } -} - -export class FormSubmitGenerator implements SignalGenerator { - id = 'form-submit' - register(emitter: SignalEmitter) { - const handleSubmit = (ev: SubmitEvent) => { - const target = ev.target as HTMLFormElement | null - - if (!target) return - - // reference to the form element that the submit event is being fired at - const submitter = ev.submitter - // If the form is submitted via JavaScript using form.submit(), the submitter property will be null because no specific button/input triggered the submission. - emitter.emit( - createInteractionSignal({ - eventType: 'submit', - target: parseElement(target), - submitter: submitter ? parseElement(submitter) : undefined, - }) - ) - } - document.addEventListener('submit', handleSubmit, true) - return () => document.removeEventListener('submit', handleSubmit) - } -} - -export const shouldIgnoreElement = (el: HTMLElement): boolean => { - if (el instanceof HTMLInputElement) { - return el.type === 'password' - } - return false -} diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/element-parser.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/element-parser.ts deleted file mode 100644 index 9fb036f06..000000000 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/element-parser.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { cleanText } from './helpers' -import type { ParsedAttributes } from '@segment/analytics-signals-runtime' - -interface Label { - textContent: string - id: string - attributes: ParsedAttributes -} - -const parseFormData = (data: FormData): Record => { - return [...data].reduce((acc, [key, value]) => { - if (typeof value === 'string') { - acc[key] = value - } - return acc - }, {} as Record) -} - -const parseLabels = ( - labels: NodeListOf | null | undefined -): Label[] => { - if (!labels) return [] - return [...labels].map(parseToLabel).filter((el): el is Label => Boolean(el)) -} - -const parseToLabel = (label: HTMLElement): Label => { - const textContent = label.textContent ? cleanText(label.textContent) : '' - return { - id: label.id, - attributes: parseNodeMap(label.attributes), - textContent, - } -} - -const parseNodeMap = (nodeMap: NamedNodeMap): ParsedAttributes => { - return Array.from(nodeMap).reduce((acc, attr) => { - if (typeof attr.value === 'string' || attr.value === null) { - acc[attr.name] = attr.value - } - return acc - }, {}) -} - -interface ParsedElementBase { - attributes: ParsedAttributes - classList: string[] - id: string - labels?: Label[] - label?: Label - name?: string - nodeName: string - tagName: string - title: string - type?: string - value?: string - textContent?: string - innerText?: string - describedBy?: Label -} - -interface ParsedSelectElement extends ParsedElementBase { - selectedOptions: { label: string; value: string }[] - selectedIndex: number -} -interface ParsedInputElement extends ParsedElementBase { - checked: boolean -} -interface ParsedMediaElement extends ParsedElementBase { - currentSrc?: string - currentTime?: number - duration: number - ended: boolean - muted: boolean - paused: boolean - playbackRate: number - readyState?: number - src?: string - volume?: number -} - -interface ParsedHTMLFormElement extends ParsedElementBase { - formData: Record - innerText: never - textContent: never -} - -type AnyParsedElement = - | ParsedHTMLFormElement - | ParsedSelectElement - | ParsedInputElement - | ParsedMediaElement - | ParsedElementBase - -const getReferencedElement = ( - el: HTMLElement, - attr: string -): HTMLElement | undefined => { - const value = el.getAttribute(attr) - if (!value) return undefined - return document.getElementById(value) ?? undefined -} - -export const parseElement = (el: HTMLElement): AnyParsedElement => { - const labels = parseLabels((el as HTMLInputElement).labels) - const labeledBy = getReferencedElement(el, 'aria-labelledby') - const describedBy = getReferencedElement(el, 'aria-describedby') - if (labeledBy) { - const label = parseToLabel(labeledBy) - labels.unshift(label) - } - - const parsedAttributes = parseNodeMap(el.attributes) - - // This exists because of a bug in react-hook-form, where 'name', if used as the field registration name overrides the native element name value to reference itself. - // This is a very weird scenario where a property was on the element, but not in the attributes map. - // This probably only needs to be run on name, but running this on some other fields out of caution. - const getSanitizedProp = (prop: string): string | undefined => { - if (!(prop in el)) { - return undefined - } - // @ts-ignore - const val = el[prop] - return typeof val === 'string' ? val : undefined - } - - const base: ParsedElementBase = { - attributes: parsedAttributes, - classList: [...el.classList], - id: getSanitizedProp('id') || '', - labels, - label: labels[0], - name: getSanitizedProp('name'), - nodeName: el.nodeName, - tagName: el.tagName, - title: getSanitizedProp('title') || '', - type: getSanitizedProp('type'), - value: getSanitizedProp('value'), - textContent: (el.textContent && cleanText(el.textContent)) ?? undefined, - innerText: (el.innerText && cleanText(el.innerText)) ?? undefined, - describedBy: (describedBy && parseToLabel(describedBy)) ?? undefined, - } - - if (el instanceof HTMLSelectElement) { - return { - ...base, - selectedOptions: [...el.selectedOptions].map((option) => ({ - value: option.value, - label: option.label, - })), - selectedIndex: el.selectedIndex, - } - } else if (el instanceof HTMLInputElement) { - return { - ...base, - checked: el.checked, - } - } else if (el instanceof HTMLMediaElement) { - return { - ...base, - currentSrc: el.currentSrc, - currentTime: el.currentTime, - duration: el.duration, - ended: el.ended, - muted: el.muted, - paused: el.paused, - playbackRate: el.playbackRate, - readyState: el.readyState, - src: el.src, - volume: el.volume, - } - } else if (el instanceof HTMLFormElement) { - return { - ...base, - innerText: undefined, - textContent: undefined, - formData: parseFormData(new FormData(el)), - } - } - return base -} diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/helpers.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/helpers.ts deleted file mode 100644 index 8030d3143..000000000 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/helpers.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const cleanText = (str: string): string => { - return str - .replace(/[\r\n\t]+/g, ' ') // Replace newlines and tabs with a space - .replace(/\s\s+/g, ' ') // Replace multiple spaces with a single space - .replace(/\u00A0/g, ' ') // Replace non-breaking spaces with a regular space - .trim() // Trim leading and trailing spaces -} - -// Check if a subset object is a partial match of another object -export const isObjectMatch = >( - partialObj: Partial, - mainObj: Obj -): boolean => { - return Object.keys(partialObj).every( - (key) => partialObj[key as keyof Obj] === mainObj[key as keyof Obj] - ) -} diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/index.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/index.ts deleted file mode 100644 index 7107d575b..000000000 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ClickSignalsGenerator, FormSubmitGenerator } from './dom-gen' -import { - MutationChangeGenerator, - OnChangeGenerator, - ContentEditableChangeGenerator, -} from './change-gen' -import { SignalGeneratorClass } from '../types' -import { OnNavigationEventGenerator } from './navigation-gen' - -export const domGenerators: SignalGeneratorClass[] = [ - MutationChangeGenerator, - OnChangeGenerator, - ContentEditableChangeGenerator, - ClickSignalsGenerator, - FormSubmitGenerator, - OnNavigationEventGenerator, -] diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts deleted file mode 100644 index 63d8e345d..000000000 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/mutation-observer.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { Emitter } from '@segment/analytics-generic-utils' -import { exists } from '../../../lib/exists' -import { debounceWithKey } from '../../../lib/debounce' -import { logger } from '../../../lib/logger' -import { isObjectMatch } from './helpers' - -const DEFAULT_OBSERVED_ATTRIBUTES = [ - 'aria-pressed', - 'aria-checked', - 'aria-modal', - 'aria-selected', - 'value', - 'checked', - 'data-selected', -] -const DEFAULT_OBSERVED_TAGS = ['input', 'label', 'option', 'select', 'textarea'] -const DEFAULT_OBSERVED_ROLES = [ - 'button', - 'checkbox', - 'dialog', - 'gridcell', - 'row', - 'searchbox', - 'menuitemcheckbox', - 'menuitemradio', - 'option', - 'radio', - 'scrollbar', - 'slider', - 'spinbutton', - 'switch', - 'tab', - 'treeitem', -] - -type AttributeMutations = { [attributeName: string]: string | null } -export type AttributeChangedEvent = { - element: HTMLElement - attributes: AttributeMutations -} - -export interface MutationObservableSettingsConfig { - extraSelectors?: string[] - pollIntervalMs?: number - debounceMs?: number - emitInputStrategy?: 'debounce-only' | 'blur' // the blur strategy seems to have an issue where it does not alwaus register when the page loads? It's also pretty finicky / manual. - observedRoles?: (defaultObservedRoles: string[]) => string[] - observedTags?: (defaultObservedTags: string[]) => string[] - observedAttributes?: (defaultObservedAttributes: string[]) => string[] -} - -export class MutationObservableSettings { - pollIntervalMs: number - debounceMs: number - emitInputStrategy: 'debounce-only' | 'blur' - extraSelectors: string[] - observedRoles: string[] - observedTags: string[] - observedAttributes: string[] - constructor(config: MutationObservableSettingsConfig = {}) { - const { - pollIntervalMs = 400, - debounceMs = 1000, - emitInputStrategy = 'debounce-only', - extraSelectors = [], - observedRoles, - observedTags, - observedAttributes, - } = config - if (pollIntervalMs < 300) { - throw new Error('Poll interval must be at least 300ms') - } - - this.emitInputStrategy = emitInputStrategy - this.pollIntervalMs = pollIntervalMs - this.debounceMs = debounceMs - this.extraSelectors = extraSelectors - - this.observedRoles = observedRoles - ? observedRoles(DEFAULT_OBSERVED_ROLES) - : DEFAULT_OBSERVED_ROLES - this.observedTags = observedTags - ? observedTags(DEFAULT_OBSERVED_TAGS) - : DEFAULT_OBSERVED_TAGS - this.observedAttributes = observedAttributes - ? observedAttributes(DEFAULT_OBSERVED_ATTRIBUTES) - : DEFAULT_OBSERVED_ATTRIBUTES - } -} - -const shouldDebounce = (el: HTMLElement): boolean => { - const MUTABLE_INPUT_TYPES = new Set([ - 'text', - 'password', - 'email', - 'url', - 'tel', - 'number', - 'search', - 'date', - 'time', - 'datetime-local', - 'month', - 'week', - 'color', - 'range', - null, // same as 'text' - ]) - - const ROLES = new Set(['spinbutton']) - const isInput = - el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement - - const isContentEditable = el.isContentEditable - if (isContentEditable) { - return true - } - if (!isInput) { - return false - } - - const type = el.getAttribute('type') - if (MUTABLE_INPUT_TYPES.has(type)) { - return true - } - const role = el.getAttribute('role') - if (role && ROLES.has(role)) { - return true - } - return false -} - -export type MutationObservableSubscriber = ( - event: AttributeChangedEvent -) => void -/** - * This class is responsible for observing changes to elements in the DOM - * This is preferred over monitoring document 'change' events, as it captures changes to custom elements - */ -export class MutationObservable { - private settings: MutationObservableSettings - // Track observed elements to avoid duplicate observers - // WeakSet is used here to allow garbage collection of elements that are no longer in the DOM - private observedElements = new WeakSet() - private prevMutationsCache = new WeakMap() - private emitter = new ElementChangedEmitter() - private listeners = new Set() - - subscribe(fn: MutationObservableSubscriber) { - this.listeners.add(fn) - this.emitter.on('attributeChanged', fn) - } - - cleanup() { - this.listeners.forEach((fn) => this.emitter.off('attributeChanged', fn)) - this.listeners.clear() - clearInterval(this.pollTimeout) - } - - private pollTimeout: ReturnType - - constructor(settings?: MutationObservableSettings) { - this.settings = settings ?? new MutationObservableSettings() - - this.checkForNewElements(this.emitter) - - this.pollTimeout = setInterval( - () => this.checkForNewElements(this.emitter), - this.settings.pollIntervalMs - ) - } - - private shouldEmitEvent( - attributeName: string, - newValue: string | null - ): boolean { - // Filter out aria-selected events where the new value is false, since there will always be another selected value -- otherwise, checked would/should be used - if (attributeName === 'aria-selected' && newValue === 'false') { - return false - } - return true - } - - private experimentalOnChangeAdapter = new ExperimentalOnChangeEventAdapter() - - private observeElementAttributes( - element: Element, - attributes: string[], - emitter: ElementChangedEmitter - ) { - if (!(element instanceof HTMLElement)) { - return - } - const _emitAttributeMutationEvent = (attributes: AttributeMutations) => { - emitter.emit('attributeChanged', { - element, - attributes, - }) - } - const addOnBlurListener = (attributeMutations: AttributeMutations) => - this.experimentalOnChangeAdapter.onBlur(element, () => - _emitAttributeMutationEvent(attributeMutations) - ) - - const emit = - this.settings.emitInputStrategy === 'blur' - ? addOnBlurListener - : _emitAttributeMutationEvent - - const shouldDebounceElement = shouldDebounce(element) - - const _emitMaybeDebouncedEvent = shouldDebounceElement - ? debounceWithKey( - emit, - // debounce based on the attribute names, so that we can debounce all changes to a single attribute. e.g if attribute "value" changes, that gets debounced, but if another attribute changes, that gets debounced separately - (m) => Object.keys(m).sort(), - this.settings.debounceMs - ) - : _emitAttributeMutationEvent - - // any call to setAttribute triggers a mutation event - const cb: MutationCallback = (mutationsList) => { - const mutations: AttributeMutations = mutationsList - .filter((m) => m.type === 'attributes') - .map((m) => { - const attributeName = m.attributeName - const target = m.target - if (!attributeName || !target || !(target instanceof HTMLElement)) - return - - const newValue = target.getAttribute(attributeName) - const v = { - attributeName, - newValue: newValue, - } as const - logger.debug('Attribute mutation', { - newValue, - oldValue: m.oldValue, - target: m.target, - }) - return v - }) - .filter(exists) - .filter((event) => - this.shouldEmitEvent(event.attributeName, event.newValue) - ) - .reduce((acc, mut) => { - acc[mut.attributeName] = mut.newValue - return acc - }, {}) - - const isEmpty = Object.keys(mutations).length === 0 - if (isEmpty) { - return - } - - // only emit if there are actual change to an attribute. - // in mutationObserver, setAttribute('value', ''), setAttribute('value', '') will both trigger a mutation event - // if the value is the same as the last one emitted from a given element, we don't want to emit it again - const prevMutations = this.prevMutationsCache.get(element) - if (prevMutations) { - const hasActuallyChanged = !isObjectMatch(mutations, prevMutations) - if (!hasActuallyChanged) { - return - } - } - - this.prevMutationsCache.set(element, { - ...prevMutations, - ...mutations, - }) - - _emitMaybeDebouncedEvent(mutations) - } - - const observer = new MutationObserver(cb) - - observer.observe(element, { - attributes: true, - attributeFilter: attributes, - subtree: false, - }) - - this.observedElements.add(element) - } - - private checkForNewElements(emitter: ElementChangedEmitter) { - const allElementSelectors = [ - ...this.settings.observedRoles.map((role) => `[role="${role}"]`), - ...this.settings.observedTags, - ...this.settings.extraSelectors, - ] - allElementSelectors.forEach((selector) => { - const elements = document.querySelectorAll(selector) - elements.forEach((element) => { - if (this.observedElements.has(element)) { - return - } - logger.debug('Observing element', element) - this.observeElementAttributes( - element, - this.settings.observedAttributes, - emitter - ) - }) - }) - } -} - -/** - * This class is responsible for normalizing listener behavior so that events are only emitted once -- just like 'change' events - */ -class ExperimentalOnChangeEventAdapter { - private inputListeners: Map = new Map() - private removeListener(element: HTMLElement) { - const oldListener = this.inputListeners.get(element) - if (oldListener) { - element.removeEventListener('blur', oldListener) - } - } - onBlur(element: HTMLElement, cb: () => void) { - this.removeListener(element) - element.addEventListener('blur', cb, { once: true }) // once: true is important here, otherwise we'd get duplicate events if someone clicks out of the input and then back in - // on 'enter' keydown, we also want to emit the event - element.addEventListener( - 'keydown', - (event) => { - if (event.key === 'Enter') { - cb() - } - }, - { once: true } - ) - this.inputListeners.set(element, cb) - } -} - -type EmitterContract = { - attributeChanged: [AttributeChangedEvent] -} -class ElementChangedEmitter extends Emitter {} diff --git a/packages/signals/signals/src/core/signal-generators/dom-gen/navigation-gen.ts b/packages/signals/signals/src/core/signal-generators/dom-gen/navigation-gen.ts deleted file mode 100644 index 538492798..000000000 --- a/packages/signals/signals/src/core/signal-generators/dom-gen/navigation-gen.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { ChangedProperties } from '@segment/analytics-signals-runtime' -import { URLChangeObservable } from '../../../lib/detect-url-change' -import { createNavigationSignal } from '../../../types/factories' -import { SignalEmitter } from '../../emitter' -import { SignalGenerator } from '../types' - -function getChangedProperties(url1: URL, url2: URL): ChangedProperties[] { - const changed: ChangedProperties[] = [] - const propertiesToCompare = ['pathname', 'search', 'hash'] as const - - for (const property of propertiesToCompare) { - if (url1[property] !== url2[property]) { - if (property === 'pathname') { - changed.push('path') - } else { - changed.push(property) - } - } - } - - return changed -} - -export class OnNavigationEventGenerator implements SignalGenerator { - id = 'navigation' - - urlChange: URLChangeObservable - constructor() { - this.urlChange = new URLChangeObservable() - } - - register(emitter: SignalEmitter): () => void { - emitter.emit( - createNavigationSignal({ - ...this.createCommonFields(), - }) - ) - - // emit a navigation signal whenever the URL has changed - this.urlChange.subscribe(({ previous, current }) => - emitter.emit( - createNavigationSignal({ - previousUrl: previous.href, - changedProperties: getChangedProperties(current, previous), - ...this.createCommonFields(), - }) - ) - ) - - return () => { - this.urlChange.unsubscribe() - } - } - - private createCommonFields() { - return { - // these fields are named after those from the page call, rather than a DOM api. - currentUrl: location.href, - path: location.pathname, - hash: location.hash, - search: location.search, - title: document.title, - } - } -} diff --git a/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/helpers.test.ts b/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/helpers.test.ts deleted file mode 100644 index ac9fd839d..000000000 --- a/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/helpers.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { setLocation } from '../../../../test-helpers/set-location' -import { containsContentType, isSameDomain } from '../helpers' - -describe(isSameDomain, () => { - it('should work if both domains are subdomains', () => { - setLocation({ hostname: 'hello.bar.com' }) - expect(isSameDomain('https://foo.bar.com')).toBe(true) - }) - - it('should handle common uses cases with country code TLDs', () => { - setLocation({ hostname: 'hello.foo.co.uk' }) - expect(isSameDomain('https://foo.co.uk')).toBe(true) - - setLocation({ hostname: 'hello.baz.foo.co.uk' }) - expect(isSameDomain('https://bar.foo.co.uk')).toBe(true) - - setLocation({ hostname: 'hello.baz.something-else.co.uk' }) - expect(isSameDomain('https://bar.foo.co.uk')).toBe(false) - }) - - it('should only match first party domains', () => { - setLocation({ hostname: 'example.com' }) - expect(isSameDomain('https://example.com')).toBe(true) - expect(isSameDomain('https://www.example.com')).toBe(true) - expect(isSameDomain('https://www.example.com/api/foo')).toBe(true) - expect(isSameDomain('https://www.foo.com')).toBe(false) - expect( - isSameDomain('https://cdn.segment.com/v1/projects/1234/versions/1') - ).toBe(false) - }) - - it('should work with subdomains', () => { - setLocation({ hostname: 'api.example.com' }) - expect(isSameDomain('https://api.example.com/foo')).toBe(true) - expect(isSameDomain('https://foo.com/foo')).toBe(false) - expect(isSameDomain('https://example.com/foo')).toBe(true) - }) - - it('should always allow relative domains', () => { - setLocation({ hostname: 'example.com' }) - expect(isSameDomain('/foo/bar')).toBe(true) - expect(isSameDomain('foo/bar')).toBe(true) - expect(isSameDomain('foo')).toBe(true) - }) - - it('should handle www differences', () => { - setLocation({ hostname: 'foo.previews.console.stage.twilio.com' }) - expect(isSameDomain('https://www.stage.twilio.com/foo')).toBe(true) - expect(isSameDomain('https://stage.twilio.com/foo')).toBe(true) - - setLocation({ hostname: 'www.foo.previews.console.stage.twilio.com' }) - expect(isSameDomain('https://www.stage.twilio.com/foo')).toBe(true) - expect(isSameDomain('https://stage.twilio.com/foo')).toBe(true) - expect(isSameDomain('https://bar.baz.com/foo')).toBe(false) - }) -}) - -describe(containsContentType, () => { - it('should return true if headers specified content type', () => { - const headers = new Headers({ 'content-type': 'application/json' }) - expect(containsContentType(headers, ['application/json'])).toBe(true) - }) - - it('should return true if headers one of the specified content type', () => { - const headers = new Headers({ 'content-type': 'application/json' }) - expect( - containsContentType(headers, ['application/json', 'text/html']) - ).toBe(true) - }) - it('should be case insensitive', () => { - expect( - containsContentType(new Headers({ 'Content-Type': 'application/json' }), [ - 'application/json', - ]) - ).toBe(true) - expect( - containsContentType(new Headers({ 'Content-Type': 'application/json' }), [ - 'application/json', - ]) - ).toBe(true) - }) - - it('should ignore charset', () => { - expect( - containsContentType( - new Headers({ 'Content-Type': 'application/json;charset=utf-8' }), - ['application/json'] - ) - ).toBe(true) - - expect( - containsContentType( - new Headers({ 'Content-Type': 'application/json; charset=utf-8' }), - ['application/json'] - ) - ).toBe(true) - expect( - containsContentType(new Headers({ 'Content-Type': 'application/json' }), [ - 'application/json', - ]) - ).toBe(true) - - expect( - containsContentType( - new Headers({ 'Content-Type': 'application/json ; charset=utf-8' }), - ['application/json'] - ) - ).toBe(true) - }) - - it('should return false if headers do not contain application/json', () => { - const headers = new Headers({ 'content-type': 'text/html' }) - expect(containsContentType(headers, ['application/json'])).toBe(false) - expect(containsContentType(new Headers(), ['application/json'])).toBe(false) - expect(containsContentType(undefined, ['application/json'])).toBe(false) - }) -}) diff --git a/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-generator.test.ts b/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-generator.test.ts deleted file mode 100644 index abb5f06e1..000000000 --- a/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-generator.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { NetworkGenerator } from '..' -import { SignalEmitter } from '../../../emitter' -import { Response } from 'node-fetch' -import { sleep } from '@internal/test-helpers' -import { setLocation } from '../../../../test-helpers/set-location' -import { NetworkSignal } from '@segment/analytics-signals-runtime' - -// xhr tests are in integration tests -describe(NetworkGenerator, () => { - class TestNetworkGenerator extends NetworkGenerator {} - - beforeEach(() => { - setLocation({ hostname: 'localhost' }) - // @ts-ignore - const mockFetch: typeof fetch = (input, init) => { - return Promise.resolve( - new Response(JSON.stringify({ data: 'test' }), { - headers: { 'content-type': 'application/json' }, - url: input.toString(), - }) - ) - } - - window.fetch = mockFetch - }) - - it('should emit response signal and request signal, regardless of content type', async () => { - const mockEmitter = { emit: jest.fn() } - const networkGenerator = new TestNetworkGenerator() - const unregister = networkGenerator.register( - mockEmitter as unknown as SignalEmitter - ) - - await window.fetch(`/api`, { - method: 'POST', - headers: { 'content-type': 'text/html' }, - body: 'hello world', - }) - - await sleep(100) - expect( - mockEmitter.emit.mock.calls.flatMap((call) => - call.map((s: NetworkSignal) => s.data.action) - ) - ).toMatchInlineSnapshot(` - [ - "request", - "response", - ] - `) - - unregister() - }) - - it('should emit response signal and request signal if no content-type', async () => { - const mockEmitter = { emit: jest.fn() } - const networkGenerator = new TestNetworkGenerator() - const unregister = networkGenerator.register( - mockEmitter as unknown as SignalEmitter - ) - - await window.fetch(`/some-data?foo=123`, { - method: 'GET', - }) - - await sleep(100) - expect( - mockEmitter.emit.mock.calls.flatMap((call) => - call.map((s: NetworkSignal) => s.data.action) - ) - ).toMatchInlineSnapshot(` - [ - "request", - "response", - ] - `) - - unregister() - }) - const networkSignalMatcher = { - data: { requestId: expect.stringMatching(/.+/) }, - } - it('should register and emit signals on fetch requests and responses if on same domain', async () => { - const mockEmitter = { emit: jest.fn() } - const networkGenerator = new TestNetworkGenerator() - const unregister = networkGenerator.register( - mockEmitter as unknown as SignalEmitter - ) - - await window.fetch(`http://${window.location.hostname}/test`, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ key: 'value' }), - }) - - await sleep(100) - expect(mockEmitter.emit.mock.calls.length).toBe(2) - const [first, second] = mockEmitter.emit.mock.calls - - expect(first[0]).toMatchInlineSnapshot( - networkSignalMatcher, - ` - { - "anonymousId": "", - "context": { - "library": { - "name": "@segment/analytics-next", - "version": "0.0.0", - }, - "signalsRuntime": "", - }, - "data": { - "action": "request", - "body": { - "key": "value", - }, - "contentType": "application/json", - "method": "POST", - "page": { - "hash": "", - "hostname": "localhost", - "path": "/", - "referrer": "", - "search": "", - "title": "", - "url": "http://localhost/", - }, - "requestId": StringMatching /\\.\\+/, - "url": "http://localhost/test", - }, - "index": undefined, - "timestamp": , - "type": "network", - } - ` - ) - - expect(second[0]).toMatchInlineSnapshot( - networkSignalMatcher, - ` - { - "anonymousId": "", - "context": { - "library": { - "name": "@segment/analytics-next", - "version": "0.0.0", - }, - "signalsRuntime": "", - }, - "data": { - "action": "response", - "body": { - "data": "test", - }, - "contentType": "application/json", - "ok": true, - "page": { - "hash": "", - "hostname": "localhost", - "path": "/", - "referrer": "", - "search": "", - "title": "", - "url": "http://localhost/", - }, - "requestId": StringMatching /\\.\\+/, - "status": 200, - "url": "http://localhost/test", - }, - "index": undefined, - "timestamp": , - "type": "network", - } - ` - ) - - unregister() - }) - - it('should default to GET method if no method is provided', async () => { - const mockEmitter = { emit: jest.fn() } - const networkGenerator = new TestNetworkGenerator() - const unregister = networkGenerator.register( - mockEmitter as unknown as SignalEmitter - ) - - await window.fetch(`/test`, { - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ key: 'value' }), - }) - - await sleep(100) - const [first] = mockEmitter.emit.mock.calls - - expect(first[0].data.method).toBe('GET') - unregister() - }) -}) diff --git a/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-interceptor.test.ts b/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-interceptor.test.ts deleted file mode 100644 index b80b53e4f..000000000 --- a/packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-interceptor.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { - NetworkInterceptor, - NetworkRequestHandler, - NetworkResponseHandler, -} from '../network-interceptor' -import { Response } from 'node-fetch' -import { EventEmitter } from 'events' - -describe(NetworkInterceptor, () => { - let interceptor: NetworkInterceptor - - afterEach(() => { - interceptor.cleanup() - }) - - const mockRequestHandler: jest.MockedFn = jest.fn() - const mockResponseHandler: jest.MockedFn = jest.fn() - - it('should intercept fetch requests and responses', async () => { - interceptor = new NetworkInterceptor() - const mockResponse = new Response(JSON.stringify({ data: 'test' }), { - headers: { 'Content-Type': 'application/json' }, - }) - - window.fetch = jest.fn().mockResolvedValue(mockResponse) - - interceptor.addInterceptors(mockRequestHandler, mockResponseHandler) - - await window.fetch('http://example.com') - - expect(mockRequestHandler).toHaveBeenCalled() - expect(mockResponseHandler).toHaveBeenCalled() - }) - - it('should return the actual response, not a cloned response', async () => { - // we don't want to quietly break the users fetch implementation - interceptor = new NetworkInterceptor() - const mockResponse = new Response(JSON.stringify({ data: 'test' }), { - headers: { 'Content-Type': 'application/json' }, - }) - - window.fetch = jest.fn().mockResolvedValue(mockResponse) - - interceptor.addInterceptors( - () => {}, - async (r) => { - // in any Response object, .text() / .json() etc are single use only -- which is why we do response.clone(). - // https://developer.mozilla.org/en-US/docs/Web/API/Response/clone - await r.body() - } - ) - - const response = await window.fetch('http://example.com') - return expect(() => response.json()).not.toThrow() - }) - - // Very primitive mock for XMLHttpRequest -- better tests are at the integration level - it('should intercept XHR requests and responses', async () => { - interface XMLHttpRequestMock { - open: XMLHttpRequest['open'] - send: XMLHttpRequest['send'] - setRequestHeader: XMLHttpRequest['setRequestHeader'] - getAllResponseHeaders: XMLHttpRequest['getAllResponseHeaders'] - addEventListener: XMLHttpRequest['addEventListener'] - onreadystatechange: XMLHttpRequest['onreadystatechange'] - } - - class MockXMLHttpRequest implements XMLHttpRequestMock { - UNSENT = 0 - OPENED = 1 - HEADERS_RECEIVED = 2 - LOADING = 3 - DONE = 4 - - private _emitter: EventEmitter - public readyState = 0 - public status = 0 - public responseText = '' - public responseURL = '' - private _responseHeaders = '' - public onreadystatechange: () => void = () => undefined - constructor() { - this._emitter = new EventEmitter().on('readystatechange', () => { - this.onreadystatechange() - }) - } - - open(_method: string, url: string) { - this.responseURL = url - } - - send() { - setTimeout(() => { - this.readyState = this.HEADERS_RECEIVED - this._emitter.emit('readystatechange') - }, 20) - - setTimeout(() => { - this.readyState = this.LOADING - this._emitter.emit('readystatechange') - }, 30) - - setTimeout(() => { - this.readyState = this.DONE - this.status = 200 - this.responseText = JSON.stringify({ data: 'test' }) - this.responseURL = 'http://example.com' - this._responseHeaders = - [ - 'content-type: application/json; charset=utf-8', - 'cache-control: max-age=3600', - 'x-content-type-options: nosniff', - 'date: Mon, 18 Nov 2000 12:00:00 GMT', - ].join('\r\n') + '\r\n' // trailing CRLF to be realistic - - this._emitter.emit('readystatechange') - }, 40) - } - - setRequestHeader() { - // no-op - } - - addEventListener(event: string, listener: (ev: any) => void) { - this._emitter.on(event, listener) - } - - getAllResponseHeaders() { - if (!this._responseHeaders) - throw new Error( - 'Headers not set yet, please run this after the response is received' - ) - return this._responseHeaders - } - } - - ;(globalThis as any).XMLHttpRequest = MockXMLHttpRequest - interceptor = new NetworkInterceptor() - - interceptor.addInterceptors(mockRequestHandler, mockResponseHandler) - - const xhr = new XMLHttpRequest() - xhr.open('POST', 'http://example.com') - xhr.setRequestHeader('accept', 'application/json') - xhr.setRequestHeader('x-something-else', 'foo') - xhr.send() - - await new Promise((resolve) => setTimeout(resolve, 100)) - - expect(mockRequestHandler).toHaveBeenCalled() - const request = mockRequestHandler.mock.calls[0][0] - expect(request.headers).toBeInstanceOf(Headers) - expect(request.headers!.get('accept')).toBe('application/json') - expect(request.headers!.get('x-something-else')).toBe('foo') - expect(mockResponseHandler).toHaveBeenCalled() - const response = mockResponseHandler.mock.calls[0][0] - expect(response.headers).toBeInstanceOf(Headers) - expect(response.headers.get('content-type')).toBe( - 'application/json; charset=utf-8' - ) - expect(response.headers.get('date')).toBe('Mon, 18 Nov 2000 12:00:00 GMT') - expect(response.url).toBe('http://example.com') - expect(response.status).toBe(200) - }) -}) diff --git a/packages/signals/signals/src/core/signal-generators/network-gen/helpers.ts b/packages/signals/signals/src/core/signal-generators/network-gen/helpers.ts deleted file mode 100644 index d3319513d..000000000 --- a/packages/signals/signals/src/core/signal-generators/network-gen/helpers.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { JSONArray, JSONObject } from '@segment/analytics-next' - -/** - * @example example.foo.bar.com => example.com - * @example example.foo.co.uk => example.co.uk - */ -export const getDomainFromHost = (hostName: string): string => { - const modified = hostName.replace('www.', '') - const parts = modified.split('.') - let offset = 2 - if ( - // handle .co.uk, .com.au, .org.uk, .eu.com etc - // this is a naive approach, but it's good enough for our use case - ['co', 'com', 'org', 'eu'].includes(parts[parts.length - 2]) - ) { - offset = 3 - } - return parts.slice(-offset).join('.') -} - -export const isSameDomain = (url: string): boolean => { - // Relative URL like /foo/bar will always be considered same domain - const rIsAbs = new RegExp('^(?:[a-z+]+:)?//', 'i') - if (!rIsAbs.test(url)) { - return true - } - const host = new URL(url).hostname - return getDomainFromHost(host) === getDomainFromHost(window.location.hostname) -} - -export const normalizeHeaders = (headers: HeadersInit): Headers => { - return headers instanceof Headers ? headers : new Headers(headers) -} - -/** - * @example expect(containsContentType(headers, ['application/json'])).toBe(true) - */ -export const containsContentType = ( - headers: HeadersInit | undefined, - match: string[] -): boolean => { - if (!headers) { - return false - } - const normalizedHeaders = normalizeHeaders(headers) - return match.some((t) => normalizedHeaders.get('content-type')?.includes(t)) -} - -export const containsJSONContentType = ( - Headers: HeadersInit | undefined -): boolean => { - return containsContentType(Headers, [ - 'application/json', - 'application/ld+json', - 'text/json', - ]) -} - -export const containsJSONParseableContentType = ( - headers: HeadersInit | undefined -): boolean => { - return ( - containsJSONContentType(headers) || - containsContentType(headers, ['text/plain']) - ) -} - -export const isOk = (status: number) => status >= 200 && status < 300 - -/** - * Safely parse JSON, if it fails, return the original text. - */ -export const tryJSONParse = (text: string): JSONObject | JSONArray | string => { - try { - return JSON.parse(text) - } catch (err) { - return text - } -} - -/** - * Normalize the first parameter of a fetch request - */ -export const normalizeRequestInfo = (requestInfo: RequestInfo | URL) => { - if (typeof requestInfo === 'string') { - return requestInfo - } else if ('url' in requestInfo) { - return requestInfo.url - } else { - return requestInfo.toString() - } -} - -export const createRequestId = () => { - return Math.random().toString(36).substring(3) -} 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 deleted file mode 100644 index fded69bf9..000000000 --- a/packages/signals/signals/src/core/signal-generators/network-gen/index.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { logger } from '../../../lib/logger' -import { createNetworkSignal } from '../../../types/factories' -import { SignalEmitter } from '../../emitter' -import { SignalGenerator } from '../types' -import { - NetworkInterceptor, - NetworkRequestHandler, - NetworkResponseHandler, -} from './network-interceptor' -import { containsJSONContentType, tryJSONParse } from './helpers' - -export class NetworkGenerator implements SignalGenerator { - id = 'network' - - private interceptor = new NetworkInterceptor() - /* List of all signal request IDs that have been emitted */ - private emittedRequestIds: string[] = [] - - register(emitter: SignalEmitter) { - const handleRequest: NetworkRequestHandler = (rq) => { - if (!rq.url) { - return - } - - const body = typeof rq.body === 'string' ? tryJSONParse(rq.body) : null - - this.emittedRequestIds.push(rq.id) - emitter.emit( - createNetworkSignal({ - action: 'request', - url: rq.url, - method: rq.method || 'GET', - body, - contentType: rq.headers?.get('content-type') || '', - requestId: rq.id, - }) - ) - } - - const handleResponse: NetworkResponseHandler = async (rs) => { - const isSuccessWithNonJSONResponse = - rs.ok && - rs.responseType !== 'json' && - !containsJSONContentType(rs.headers) - - const isErrorButRequestNeverEmittted = - !rs.ok && !this.emittedRequestIds.includes(rs.req.id) - - if (isSuccessWithNonJSONResponse || isErrorButRequestNeverEmittted) { - return - } - const url = rs.url - if (!url) { - return - } - - const data = await rs.body() - - emitter.emit( - createNetworkSignal({ - action: 'response', - url, - body: data, - ok: rs.ok, - status: rs.status, - contentType: rs.headers.get('content-type') || '', - requestId: rs.req.id, - }) - ) - } - this.interceptor.addInterceptors(handleRequest, handleResponse) - - return () => { - this.interceptor.cleanup() - logger.debug('Removing fetch interceptor') - } - } -} diff --git a/packages/signals/signals/src/core/signal-generators/network-gen/network-interceptor.ts b/packages/signals/signals/src/core/signal-generators/network-gen/network-interceptor.ts deleted file mode 100644 index 9544bbd8c..000000000 --- a/packages/signals/signals/src/core/signal-generators/network-gen/network-interceptor.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { JSONArray, JSONObject, JSONValue } from '@segment/analytics-next' -import { - createRequestId, - isOk, - normalizeHeaders, - normalizeRequestInfo, - tryJSONParse, -} from './helpers' - -let origFetch: typeof window.fetch -let origXMLHttpRequest: typeof XMLHttpRequest - -export type HTTPMethod = - | 'GET' - | 'POST' - | 'PUT' - | 'DELETE' - | 'PATCH' - | 'HEAD' - | 'OPTIONS' - -const toHTTPMethod = (method: string): HTTPMethod => { - return method.toUpperCase() as HTTPMethod -} - -export interface NetworkInterceptorRequest { - url: string - method: HTTPMethod - contentType: string | undefined - body: string | undefined - headers: Headers - id: string -} - -export interface NetworkInterceptorResponse { - url: string - status: number - ok: boolean - statusText: string - headers: Headers - body: () => Promise - initiator: 'fetch' | 'xhr' - responseType: XMLHttpRequestResponseType | undefined - req: { - id: string - } -} - -const createInterceptorRequest = ({ - url, - body, - headers, - id, - method, -}: { - url: URL | string - body: string | undefined - method: HTTPMethod - headers: Headers | undefined - id: string -}): NetworkInterceptorRequest => ({ - url: url.toString(), - method: method, - headers: headers ?? new Headers(), - contentType: headers?.get('content-type') ?? undefined, - body: typeof body == 'string' ? body : undefined, - id, -}) - -const createInterceptorResponse = ({ - body, - headers, - initiator, - status, - statusText, - url, - responseType, - id, -}: { - body: () => Promise - headers: Headers - initiator: 'fetch' | 'xhr' - status: number - statusText: string - url: string - responseType: XMLHttpRequestResponseType | undefined - id: string -}): NetworkInterceptorResponse => ({ - body, - headers, - initiator, - ok: isOk(status), - status, - statusText, - url, - responseType, - req: { - id: id, - }, -}) - -/** - * Taken from https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response - */ - -export interface NetworkResponseHandler { - (rs: NetworkInterceptorResponse): void -} - -export interface NetworkRequestHandler { - (rq: NetworkInterceptorRequest): void -} - -/** - * NetworkInterceptor class to intercept network requests and responses - */ -export class NetworkInterceptor { - addInterceptors( - onRequest: NetworkRequestHandler, - onResponse: NetworkResponseHandler - ) { - this.addFetchInterceptor(onRequest, onResponse) - this.addXhrInterceptor(onRequest, onResponse) - } - private addFetchInterceptor( - onRequest: NetworkRequestHandler, - onResponse: NetworkResponseHandler - ) { - if (!window.fetch) { - return - } - origFetch = window.fetch - window.fetch = async (...args) => { - const [url, options] = args - const id = createRequestId() - try { - const normalizedURL = normalizeRequestInfo(url) - const headers = options?.headers - ? normalizeHeaders(options.headers) - : undefined - - onRequest( - createInterceptorRequest({ - url: normalizedURL, - body: typeof options?.body == 'string' ? options.body : undefined, - method: options?.method ? toHTTPMethod(options.method) : 'GET', - headers, - id, - }) - ) - } catch (err) { - console.log('Error handling request: ', err) - } - const ogResponse = await origFetch(...args) - - // response.text() etc is single use only -- to prevent conflicts with app code, use clone - // https://developer.mozilla.org/en-US/docs/Web/API/Response/clone - const response = ogResponse.clone() - - try { - const lazyBody = async (): Promise => { - let text: string - try { - text = await response.text() - } catch (err) { - console.warn('Error converting to text', err, response) - return null - } - return tryJSONParse(text) // should never throw. - } - - onResponse( - createInterceptorResponse({ - body: lazyBody, - headers: response.headers, - initiator: 'fetch', - status: response.status, - statusText: response.statusText, - url: response.url, - responseType: undefined, - id: id, - }) - ) - } catch (err) { - console.log('Error handling response: ', err) - } - return ogResponse - } - } - - private addXhrInterceptor( - onRequest: NetworkRequestHandler, - onResponse: NetworkResponseHandler - ) { - if (!window.XMLHttpRequest) { - return - } - const OrigXMLHttpRequest = window.XMLHttpRequest - class InterceptedXMLHttpRequest extends OrigXMLHttpRequest { - _reqURL?: string - _reqMethod?: HTTPMethod - _reqBody?: XMLHttpRequestBodyInit - _reqHeaders?: Headers - _reqId = createRequestId() - - private getParsedXHRHeaders(allResponseHeaders: string): Headers { - const headers = new Headers() - allResponseHeaders - .trim() - .split(/[\r\n]+/) - .forEach((line) => { - const parts = line.split(': ') - const header = parts.shift() - const value = parts.join(': ') - if (header) { - headers.append(header, value) - } - }) - return headers - } - - private getParsedXHRBody(): JSONValue { - if (this.responseType === 'json') { - return this.response as JSONObject | JSONArray | null - } else if (typeof this.response === 'string') { - return tryJSONParse(this.response) - } - return null - } - - constructor() { - super() - - this.addEventListener('readystatechange', () => { - // Handle request - if (this.readyState === this.HEADERS_RECEIVED) { - try { - onRequest( - createInterceptorRequest({ - url: this._reqURL!, - method: this._reqMethod!, - headers: this._reqHeaders, - id: this._reqId, - body: this._reqBody ? this._reqBody.toString() : undefined, - }) - ) - } catch (err) { - console.log('Error handling request', err) - } - } - // Handle response - if (this.readyState === this.DONE) { - try { - onResponse( - createInterceptorResponse({ - status: this.status, - responseType: this.responseType, - statusText: this.statusText, - url: this.responseURL, - initiator: 'xhr', - body: () => Promise.resolve(this.getParsedXHRBody()), - headers: this.getParsedXHRHeaders( - this.getAllResponseHeaders() - ), - id: this._reqId, - }) - ) - } catch (err) { - console.log('Error handling response', err) - } - } - }) - } - // @ts-ignore - open(...args: Parameters): void { - const [method, url] = args - try { - this._reqURL = url.toString() - this._reqMethod = toHTTPMethod(method) - } catch (err) { - console.log('Error handling request (open)', err) - } - super.open(...args) - } - - send(body?: XMLHttpRequestBodyInit): void { - this._reqBody = body - super.send(body) - } - - setRequestHeader( - ...args: Parameters - ): void { - try { - const [header, value] = args - if (!this._reqHeaders) { - this._reqHeaders = new Headers() - } - this._reqHeaders.append(header, value) - } catch (err) { - console.log('Error handling request (setRequestHeader)', err) - } - super.setRequestHeader(...args) - } - } - ;(window as any).XMLHttpRequest = InterceptedXMLHttpRequest - } - - cleanup() { - origXMLHttpRequest && (window.XMLHttpRequest = origXMLHttpRequest) - origFetch && (window.fetch = origFetch) - } -} diff --git a/packages/signals/signals/src/core/signal-generators/register.ts b/packages/signals/signals/src/core/signal-generators/register.ts deleted file mode 100644 index c2f6d08b1..000000000 --- a/packages/signals/signals/src/core/signal-generators/register.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { logger } from '../../lib/logger' -import { isClass } from '../../utils/is-class' -import { SignalEmitter } from '../emitter' -import { SignalGlobalSettings } from '../signals' -import { SignalGeneratorClass, SignalGenerator } from './types' - -export const registerGenerator = async ( - emitter: SignalEmitter, - signalGenerators: (SignalGeneratorClass | SignalGenerator)[], - settings: SignalGlobalSettings -): Promise => { - const _register = (gen: SignalGeneratorClass | SignalGenerator) => { - logger.debug('Registering generator:', gen.id || (gen as any).name) - if (isClass(gen)) { - // Check if Gen is a function and has a constructor - return new gen(settings).register(emitter) - } else { - return gen.register(emitter) - } - } - - const cleanupFns = await Promise.all(signalGenerators.map(_register)) - - // Return a cleanup function that calls all the cleanup functions (e.g unsubscribes from event listeners) - return () => cleanupFns.forEach((fn) => fn()) -} diff --git a/packages/signals/signals/src/core/signal-generators/types.ts b/packages/signals/signals/src/core/signal-generators/types.ts deleted file mode 100644 index 16a594fbe..000000000 --- a/packages/signals/signals/src/core/signal-generators/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { SignalEmitter } from '../emitter' -import { SignalGlobalSettings } from '../signals' - -export interface SignalGenerator { - /** - * To support unregistering by name/label - * e.g "form-submit" - */ - id?: string - /** - * Register a custom function that emits signals. - * If this function returns a promise, signals client will not be able to send signals until the promise resolves. - */ - register(emitter: SignalEmitter): (() => void) | Promise<() => void> -} - -export interface SignalGeneratorClass { - id?: string - new (settings: SignalGlobalSettings): SignalGenerator -} diff --git a/packages/signals/signals/src/core/signals/index.ts b/packages/signals/signals/src/core/signals/index.ts deleted file mode 100644 index d11c5f6b1..000000000 --- a/packages/signals/signals/src/core/signals/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This is a barrel file, and should only contain exports. -export * from './signals' -export * from './settings' diff --git a/packages/signals/signals/src/core/signals/settings.ts b/packages/signals/signals/src/core/signals/settings.ts deleted file mode 100644 index ff30ef0a0..000000000 --- a/packages/signals/signals/src/core/signals/settings.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { parseDebugModeQueryString } from '../debug-mode' -import { logger } from '../../lib/logger' -import { SignalBufferSettingsConfig, SignalPersistentStorage } from '../buffer' -import { SignalsIngestSettingsConfig } from '../middleware/signals-ingest/signals-ingest-client' -import { SandboxSettingsConfig } from '../processor/sandbox' -import { SignalsPluginSettingsConfig } from '../../types' -import { WebStorage } from '../../lib/storage/web-storage' -import { MutationGeneratorSettings } from '../signal-generators/dom-gen/change-gen' -import { NetworkSettingsConfig } from '../middleware/network-signals-filter/network-signals-filter' - -export type SignalsSettingsConfig = Pick< - SignalsPluginSettingsConfig, - | 'middleware' - | 'maxBufferSize' - | 'apiHost' - | 'functionHost' - | 'flushAt' - | 'flushInterval' - | 'disableSignalsRedaction' - | 'enableSignalsIngestion' - | 'networkSignalsAllowList' - | 'networkSignalsDisallowList' - | 'networkSignalsAllowSameDomain' - | 'signalStorageType' - | 'mutationGenExtraSelectors' - | 'mutationGenObservedRoles' - | 'mutationGenObservedTags' - | 'mutationGenPollInterval' - | 'mutationGenObservedAttributes' - | 'debug' - | 'sandboxStrategy' -> & { - signalStorage?: SignalPersistentStorage - processSignal?: string -} - -/** - * Global settings for the application - * In the future this pattern may allows us to add settings which can be dynamically set by the user. - * Currently, this is just a way to pass settings to the different parts of the application. - */ -export class SignalGlobalSettings { - sandbox: SandboxSettingsConfig - signalBuffer: SignalBufferSettingsConfig - ingestClient: SignalsIngestSettingsConfig - network: NetworkSettingsConfig - signalsDebug: SignalsDebugSettings - mutationGenerator: MutationGeneratorSettings - private sampleSuccess = false - - constructor(settings: SignalsSettingsConfig) { - if (settings.maxBufferSize && settings.signalStorage) { - throw new Error( - 'maxBufferSize and signalStorage cannot be defined at the same time' - ) - } - - this.mutationGenerator = new MutationGeneratorSettings({ - extraSelectors: settings.mutationGenExtraSelectors, - observedRoles: settings.mutationGenObservedRoles, - observedTags: settings.mutationGenObservedTags, - pollIntervalMs: settings.mutationGenPollInterval, - observedAttributes: settings.mutationGenObservedAttributes, - }) - - this.signalsDebug = new SignalsDebugSettings( - settings.disableSignalsRedaction, - settings.enableSignalsIngestion - ) - - this.signalBuffer = { - signalStorage: settings.signalStorage, - storageType: settings.signalStorageType, - maxBufferSize: settings.maxBufferSize, - } - this.ingestClient = { - apiHost: settings.apiHost, - flushAt: settings.flushAt, - flushInterval: settings.flushInterval, - shouldDisableSignalsRedaction: - this.signalsDebug.getDisableSignalsRedaction, - shouldIngestSignals: () => { - if (this.signalsDebug.getEnableSignalsIngestion()) { - return true - } - if (this.sampleSuccess) { - return true - } - return false - }, - } - this.sandbox = { - sandboxStrategy: settings.sandboxStrategy ?? 'iframe', - functionHost: settings.functionHost, - processSignal: settings.processSignal, - edgeFnDownloadURL: undefined, - } - this.network = new NetworkSettingsConfig({ - networkSignalsAllowList: settings.networkSignalsAllowList, - networkSignalsDisallowList: settings.networkSignalsDisallowList, - networkSignalsAllowSameDomain: settings.networkSignalsAllowSameDomain, - }) - } - public update({ - edgeFnDownloadURL, - disallowListURLs, - sampleRate, - }: { - /** - * The URL to download the edge function from - */ - edgeFnDownloadURL?: string - /** - * Add new URLs to the disallow list - */ - disallowListURLs: (string | undefined)[] - /** - * Sample rate to determine sending signals - */ - sampleRate?: number - }): void { - edgeFnDownloadURL && (this.sandbox.edgeFnDownloadURL = edgeFnDownloadURL) - this.network.networkSignalsFilterList.disallowed.addURLLike( - ...disallowListURLs.filter((val: T): val is NonNullable => - Boolean(val) - ) - ) - this.sampleSuccess = this.checkSampleRate(sampleRate ?? 0) - } - private checkSampleRate(sampleRate: number): boolean { - const storage = new WebStorage(window.sessionStorage) - const previousSample = storage.getItem('segment_sample_success') - if (previousSample !== undefined) { - return previousSample - } - - if (sampleRate && Math.random() <= sampleRate) { - storage.setItem('segment_sample_success', true) - return true - } - - storage.setItem('segment_sample_success', false) - return false - } -} - -export class SignalsDebugSettings { - private static redactionKey = 'segment_signals_debug_redaction_disabled' - private static ingestionKey = 'segment_signals_debug_ingestion_enabled' - private storage = new WebStorage(window.sessionStorage) - - constructor(disableRedaction?: boolean, enableIngestion?: boolean) { - if (typeof disableRedaction === 'boolean') { - this.storage.setItem(SignalsDebugSettings.redactionKey, disableRedaction) - } - if (typeof enableIngestion === 'boolean') { - this.storage.setItem(SignalsDebugSettings.ingestionKey, enableIngestion) - } - - const debugModeInQs = parseDebugModeQueryString() - logger.debug('debugMode is set to true via query string') - if (typeof debugModeInQs === 'boolean') { - this.setAllDebugging(debugModeInQs) - } - } - - setAllDebugging = (boolean: boolean) => { - this.storage.setItem(SignalsDebugSettings.redactionKey, boolean) - this.storage.setItem(SignalsDebugSettings.ingestionKey, boolean) - } - - getDisableSignalsRedaction = (): boolean => { - return ( - this.storage.getItem(SignalsDebugSettings.redactionKey) ?? false - ) - } - - getEnableSignalsIngestion = (): boolean => { - return ( - this.storage.getItem(SignalsDebugSettings.ingestionKey) ?? false - ) - } -} diff --git a/packages/signals/signals/src/core/signals/signals.ts b/packages/signals/signals/src/core/signals/signals.ts deleted file mode 100644 index 4bda73075..000000000 --- a/packages/signals/signals/src/core/signals/signals.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { getSignalBuffer, SignalBuffer } from '../buffer' -import { SignalEmitter, SignalsMiddleware } from '../emitter' -import { domGenerators } from '../signal-generators/dom-gen' -import { NetworkGenerator } from '../signal-generators/network-gen' -import { - SignalGenerator, - SignalGeneratorClass, -} from '../signal-generators/types' -import { Signal } from '@segment/analytics-signals-runtime' -import { AnyAnalytics } from '../../types' -import { registerGenerator } from '../signal-generators/register' -import { AnalyticsService } from '../analytics-service' -import { SignalGlobalSettings, SignalsSettingsConfig } from './settings' -import { logger } from '../../lib/logger' -import { LogLevelOptions } from '../debug-mode' -import { SignalsIngestSubscriber } from '../middleware/signals-ingest' -import { SignalsEventProcessorSubscriber } from '../middleware/event-processor' -import { NetworkSignalsFilterMiddleware } from '../middleware/network-signals-filter/network-signals-filter' -import { UserInfoMiddleware } from '../middleware/user-info' - -interface ISignals { - start(analytics: AnyAnalytics): Promise - stop(): void - clearStorage(): void - registerGenerator( - generators: (SignalGeneratorClass | SignalGenerator)[] - ): Promise -} - -export type SignalsPublicEmitterContract = { - signal: [Signal] -} - -export class Signals implements ISignals { - private buffer: SignalBuffer - public signalEmitter: SignalEmitter - private cleanup: VoidFunction[] = [] - private globalSettings: SignalGlobalSettings - constructor(settingsConfig: SignalsSettingsConfig = {}) { - this.globalSettings = new SignalGlobalSettings(settingsConfig) - if (settingsConfig.debug) { - this.debug() - } - this.buffer = getSignalBuffer(this.globalSettings.signalBuffer) - this.signalEmitter = this.getSignalEmitter(settingsConfig.middleware) - - // We register the generators (along with the signal emitter) so they start collecting signals before the plugin is started. - // Otherwise, we would wait until analytics is loaded, which would skip things like page network URL changes. - void this.registerGenerator([...domGenerators, new NetworkGenerator()]) - } - - /** - * Does the following: - * - Sends any queued signals to the server. - * - Registers additional custom signal generators. - */ - async start(analytics: AnyAnalytics): Promise { - const analyticsService = new AnalyticsService(analytics) - - analyticsService.instance.on('reset', () => { - this.clearStorage() - }) - - // These settings are important to middleware configuration (e.g, they drop events) - // The middleware doesn't run until the signalEmitter is initialized -- so we need to set these settings before starting the emitter - this.globalSettings.update({ - edgeFnDownloadURL: analyticsService.edgeFnSettings?.downloadURL, - disallowListURLs: [ - analyticsService.instance.settings.apiHost, - analyticsService.instance.settings.cdnURL, - ], - sampleRate: - analyticsService.instance.settings.cdnSettings - .autoInstrumentationSettings?.sampleRate ?? 0, - }) - - await this.registerGenerator([ - analyticsService.createSegmentInstrumentationEventGenerator(), - ]) - - // load emitter and flush any queued signals to all subscribers. Register middleware - void this.signalEmitter.start({ - unstableGlobalSettings: this.globalSettings, - analyticsInstance: analyticsService.instance, - buffer: this.buffer, - }) - } - - stop() { - this.cleanup.forEach((fn) => fn()) - } - - clearStorage(): void { - void this.buffer.clear() - } - - /** - * Disable redaction, ingestion of signals, and other logging. - */ - debug(boolean = true, logLevel?: LogLevelOptions): void { - this.globalSettings.signalsDebug.setAllDebugging(boolean) - logger.enableLogging(logLevel ?? 'info') - } - - /** - * Register custom signal generators to emit signals. - */ - async registerGenerator( - generators: (SignalGeneratorClass | SignalGenerator)[] - ): Promise { - if (!this.signalEmitter) { - throw new Error('SignalEmitter not initialized') - } - if (!this.globalSettings) { - throw new Error('GlobalSettings not initialized') - } - this.cleanup.push( - await registerGenerator( - this.signalEmitter, - generators, - this.globalSettings - ) - ) - } - - private getSignalEmitter(middleware?: SignalsMiddleware[]): SignalEmitter { - // we initialize the emitter here so that registerGenerator can be called before start - return new SignalEmitter() - .addMiddleware( - new UserInfoMiddleware(), - new NetworkSignalsFilterMiddleware(), - ...(middleware || []) - ) - .subscribe( - (signal) => logger.logSignal(signal), - (signal) => this.buffer.add(signal), - new SignalsIngestSubscriber(), - new SignalsEventProcessorSubscriber() - ) - } -} diff --git a/packages/signals/signals/src/generated/version.ts b/packages/signals/signals/src/generated/version.ts deleted file mode 100644 index 798022bc2..000000000 --- a/packages/signals/signals/src/generated/version.ts +++ /dev/null @@ -1,2 +0,0 @@ -// This file is generated. -export const version = '1.13.1' diff --git a/packages/signals/signals/src/index.ts b/packages/signals/signals/src/index.ts deleted file mode 100644 index b760a722d..000000000 --- a/packages/signals/signals/src/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * This is the public API for this package. - * We avoid using splat (*) exports so that we can control what is exposed. - */ -export { SignalsPlugin } from './plugin/signals-plugin' -export { Signals } from './core/signals' -export type { Signal } from '@segment/analytics-signals-runtime' -export type { - SignalsMiddleware, - SignalsMiddlewareContext, -} from './core/emitter' -export type { - ProcessSignal, - AnalyticsRuntimePublicApi, - SignalsPluginSettingsConfig, -} from './types' diff --git a/packages/signals/signals/src/index.umd.ts b/packages/signals/signals/src/index.umd.ts deleted file mode 100644 index a3047fae1..000000000 --- a/packages/signals/signals/src/index.umd.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * This is the entry point to a webpack bundle that can be loaded via a \ No newline at end of file diff --git a/packages/signals/signals/src/lib/workerbox/worker.ts b/packages/signals/signals/src/lib/workerbox/worker.ts deleted file mode 100644 index 74cdba8ef..000000000 --- a/packages/signals/signals/src/lib/workerbox/worker.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { - createCallbackStore, - argsToString, - stringToScope, - stringToArgs, -} from './index' -async function scopedEval(context: any, expr: string) { - const evaluator = Function.apply(null, [ - ...Object.keys(context), - `return (async function sandbox () {${expr} })()`, - ]) - return await evaluator.apply(null, [...Object.values(context)]) -} - -const getStack = (error: Error, slice?: number) => { - if (!error.stack) { - return error.message - } - const lines: string[] = error.stack.split('\n') - const parseNumber = (num: string | number) => - typeof num === 'string' ? parseInt(num, 10) : num - const stack = [ - lines[0], - ...lines - .filter((line) => line.includes('(eval at scopedEval')) - .map((line) => { - const splitted = line.split('(eval at scopedEval (') - const [, mixedPosition] = line.split('') - const [, lineNumber, charNumber] = mixedPosition.slice(0, -1).split(':') - return `${splitted[0]}(:${ - parseNumber(lineNumber) - 3 - }:${charNumber})` - }), - ] - .slice(0, slice) - .join('\n') - return stack -} - -self.addEventListener('message', (event) => { - const port = event.ports[0] - - const callbacks = createCallbackStore() - const run = (id: string, args: any) => - new Promise((resolve) => { - port.postMessage([ - 'callback', - { id, args, resolve: callbacks.add(resolve) }, - ]) - }) - - port.onmessage = async (event) => { - const [action, message] = event.data - const { id, errorId, code, scope, args, resolve, reject } = message - - if (action === 'execute') { - const parsedScope = stringToScope(scope, callbacks.add, run) - - try { - const result = await scopedEval(parsedScope, code) - - port.postMessage([ - 'return', - { id, args: argsToString([result], callbacks.add, run) }, - ]) - } catch (e) { - const error = e as Error - try { - const stack = getStack(error, -1) - port.postMessage([ - 'error', - { - id: errorId, - args: argsToString([stack || error.message], callbacks.add, run), - }, - ]) - } catch (error2) { - port.postMessage([ - 'error', - { - id: errorId, - args: argsToString([error.message], callbacks.add, run), - }, - ]) - } - } - } - - if (action === 'callback') { - const parsedArgs = stringToArgs(args, callbacks.add, run) - - const fn = callbacks.get(id) - if (!fn) { - return - } - try { - const result = await fn(...parsedArgs) - port.postMessage([ - 'return', - { id: resolve, args: argsToString([result], callbacks.add, run) }, - ]) - } catch (e) { - const error = e as Error - const stack = getStack(error) - port.postMessage([ - 'error', - { - id: reject, - args: argsToString([stack || error.message], callbacks.add, run), - }, - ]) - } - } - } -}) diff --git a/packages/signals/signals/src/plugin/__tests__/signals-plugin.test.ts b/packages/signals/signals/src/plugin/__tests__/signals-plugin.test.ts deleted file mode 100644 index 72e580999..000000000 --- a/packages/signals/signals/src/plugin/__tests__/signals-plugin.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { createUserDefinedSignal } from '../../types/factories' -import { SignalsPlugin } from '../signals-plugin' - -// this specific test was throwing a bunch of warnings: -// 'Cannot read properties of null (reading '_origin') at Window.get sessionStorage [as sessionStorage]' -// no idea why, as sessionStorage works as usual in other tests. -const sessionStorageMock = (() => { - let store: Record = {} - return { - // @ts-ignore - getItem: (key: string) => store[key] || null, - setItem: (key: string, value: unknown) => { - // @ts-ignore - store[key] = value.toString() - }, - removeItem: (key: string) => { - // @ts-ignore - delete store[key] - }, - clear: () => { - store = {} - }, - } -})() - -Object.defineProperty(window, 'sessionStorage', { - value: sessionStorageMock, -}) -/** - * This should be tested at the integration level - * A few tests, just for example purposes. - */ -describe(SignalsPlugin, () => { - test('onSignal method registers callback', () => { - const plugin = new SignalsPlugin() - const callback = jest.fn() - const emitterSpy = jest.spyOn(plugin.signals.signalEmitter, 'subscribe') - plugin.onSignal(callback) - expect(emitterSpy).toHaveBeenCalledTimes(1) - expect(emitterSpy.mock.calls[0][0]).toEqual(callback) - }) - - test('addSignal method emits signal', async () => { - const plugin = new SignalsPlugin() - const emitterSpy = jest.spyOn(plugin.signals.signalEmitter, 'emit') - const userDefinedData = { foo: 'bar' } - plugin.addSignal(userDefinedData) - expect(emitterSpy).toHaveBeenCalledTimes(1) - expect(emitterSpy.mock.calls[0][0]).toMatchSignal( - createUserDefinedSignal(userDefinedData) - ) - }) -}) diff --git a/packages/signals/signals/src/plugin/signals-plugin.ts b/packages/signals/signals/src/plugin/signals-plugin.ts deleted file mode 100644 index c4dadec03..000000000 --- a/packages/signals/signals/src/plugin/signals-plugin.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { Plugin } from '@segment/analytics-next' -import { Signals } from '../core/signals' -import { logger } from '../lib/logger' -import { AnyAnalytics, SignalsPluginSettingsConfig } from '../types' -import { Signal } from '@segment/analytics-signals-runtime' -import { assertBrowserEnv } from '../lib/assert-browser-env' -import { version } from '../generated/version' -import { createUserDefinedSignal } from '../types/factories' - -export type OnSignalCb = (signal: Signal) => void - -interface SignalsAugmentedFunctionality { - stop(): void - /** - * Listen to signals - */ - onSignal: (fn: OnSignalCb) => this - - /** - * Emit/add a custom signal of type 'userDefined' - */ - addSignal(userDefinedData: Record): this -} - -export class SignalsPlugin implements Plugin, SignalsAugmentedFunctionality { - readonly type = 'utility' - readonly name = 'SignalsPlugin' - readonly version = version - public signals: Signals - constructor(settings: SignalsPluginSettingsConfig = {}) { - assertBrowserEnv() - - // assign to window.SegmentSignalsPlugin for debugging purposes (e.g window.SegmentSignalsPlugin.debug()) - Object.assign(window, { SegmentSignalsPlugin: this }) - - this.signals = new Signals({ - ...settings, - processSignal: - typeof settings.processSignal === 'function' - ? settings.processSignal.toString() - : settings.processSignal, - }) - - logger.debug(`SignalsPlugin v${version} initializing`, { - settings, - }) - } - - isLoaded() { - return true - } - - public async load(_ctx: unknown, analytics: AnyAnalytics) { - try { - await this.signals.start(analytics) - logger.debug('SignalsPlugin loaded') - } catch (err) { - console.error(err) - } - } - - stop() { - return this.signals.stop() - } - - onSignal(cb: (signal: Signal) => void): this { - this.signals.signalEmitter.subscribe(cb) - return this - } - - addSignal(data: Record): this { - this.signals.signalEmitter.emit(createUserDefinedSignal(data)) - return this - } - - /** - * Enable redaction and disable ingestion of signals. Also, logs signals to the console. - */ - debug(...args: Parameters): this { - this.signals.debug(...args) - return this - } -} diff --git a/packages/signals/signals/src/test-helpers/jest-extended.ts b/packages/signals/signals/src/test-helpers/jest-extended.ts deleted file mode 100644 index 0356cf96c..000000000 --- a/packages/signals/signals/src/test-helpers/jest-extended.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Signal } from '@segment/analytics-signals-runtime' - -export function toMatchSignal( - this: jest.MatcherContext, - received: Signal, - expected: Signal -) { - const cleanSignal = (signal: Signal) => { - const { timestamp, ...rest } = signal - return rest - } - - const pass = this.equals(cleanSignal(received), cleanSignal(expected)) - if (pass) { - return { - message: () => - `expected ${this.utils.printReceived( - received - )} not to match ${this.utils.printExpected(expected)}`, - pass: true, - } - } else { - return { - message: () => - `expected ${this.utils.printReceived( - received - )} to match ${this.utils.printExpected(expected)}`, - pass: false, - } - } -} - -/*** - * Add special matcher to compare signals - * usage: - * expect(signal).toMatchSignal(expectedSignal) - */ -expect.extend({ toMatchSignal }) - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace jest { - interface Matchers { - toMatchSignal(expected: Signal): R - } - } -} diff --git a/packages/signals/signals/src/test-helpers/mocks/analytics-mock.ts b/packages/signals/signals/src/test-helpers/mocks/analytics-mock.ts deleted file mode 100644 index 1d4b39f05..000000000 --- a/packages/signals/signals/src/test-helpers/mocks/analytics-mock.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { AnyAnalytics, EdgeFnCDNSettings } from '../../types' - -const edgeFnSettings: EdgeFnCDNSettings = { - downloadURL: 'https://foo.com', - version: 1, -} - -export const analyticsMock: jest.Mocked = { - settings: { - writeKey: 'test', - cdnSettings: { - edgeFunction: edgeFnSettings, - integrations: { - 'Segment.io': { - apiKey: '', - }, - }, - }, - }, - alias: jest.fn(), - identify: jest.fn(), - screen: jest.fn(), - group: jest.fn(), - page: jest.fn(), - track: jest.fn(), - addSourceMiddleware: jest.fn(), - reset: jest.fn(), - on: jest.fn(), - user: jest.fn().mockReturnValue({ - id: jest.fn(), - anonymousId: jest.fn(), - }), -} diff --git a/packages/signals/signals/src/test-helpers/mocks/factories.ts b/packages/signals/signals/src/test-helpers/mocks/factories.ts deleted file mode 100644 index 3cea8295c..000000000 --- a/packages/signals/signals/src/test-helpers/mocks/factories.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { TargetedHTMLElement } from '@segment/analytics-signals-runtime' - -export const createMockTarget = ( - partialTarget: Partial = {} -): TargetedHTMLElement => { - return { - attributes: {}, - classList: [], - id: 'test', - labels: [], - ...partialTarget, - } -} diff --git a/packages/signals/signals/src/test-helpers/mocks/index.ts b/packages/signals/signals/src/test-helpers/mocks/index.ts deleted file mode 100644 index e33eb167b..000000000 --- a/packages/signals/signals/src/test-helpers/mocks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './analytics-mock' diff --git a/packages/signals/signals/src/test-helpers/range.ts b/packages/signals/signals/src/test-helpers/range.ts deleted file mode 100644 index bfd1290ab..000000000 --- a/packages/signals/signals/src/test-helpers/range.ts +++ /dev/null @@ -1 +0,0 @@ -export const range = (n: number) => Array.from({ length: n }, (_, i) => i) diff --git a/packages/signals/signals/src/test-helpers/set-location.ts b/packages/signals/signals/src/test-helpers/set-location.ts deleted file mode 100644 index 142805e57..000000000 --- a/packages/signals/signals/src/test-helpers/set-location.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const setLocation = (location: Partial = {}) => { - Object.defineProperty(window, 'location', { - value: { - ...window.location, - ...location, - }, - writable: true, - }) -} 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 deleted file mode 100644 index 60d2ba832..000000000 --- a/packages/signals/signals/src/types/__tests__/create-network-signal.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { NetworkData } 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), -})) - -describe(createNetworkSignal, () => { - it('should create a network signal for a request', () => { - const body: NetworkData = { - action: 'request', - url: 'http://example.com', - method: 'POST', - body: { key: 'value' }, - contentType: 'application/json', - requestId: '12345', - } - - const signal = createNetworkSignal(body) - - expect(signal).toMatchInlineSnapshot(` - { - "anonymousId": "", - "context": { - "library": { - "name": "@segment/analytics-next", - "version": "0.0.0", - }, - "signalsRuntime": "", - }, - "data": { - "action": "request", - "body": { - "key": "value", - }, - "contentType": "application/json", - "method": "POST", - "page": { - "hash": "", - "hostname": "localhost", - "path": "/", - "referrer": "", - "search": "", - "title": "", - "url": "http://localhost/", - }, - "requestId": "12345", - "url": "http://example.com", - }, - "index": undefined, - "timestamp": , - "type": "network", - } - `) - expect(normalizeUrl).toHaveBeenCalledWith('http://example.com') - }) - - it('should create a network signal for a response', () => { - const body: NetworkData = { - action: 'response', - requestId: '12345', - url: 'http://example.com', - ok: true, - status: 200, - contentType: 'application/json', - body: { key: 'value' }, - } - - const signal = createNetworkSignal(body) - - expect(signal).toMatchInlineSnapshot(` - { - "anonymousId": "", - "context": { - "library": { - "name": "@segment/analytics-next", - "version": "0.0.0", - }, - "signalsRuntime": "", - }, - "data": { - "action": "response", - "body": { - "key": "value", - }, - "contentType": "application/json", - "ok": true, - "page": { - "hash": "", - "hostname": "localhost", - "path": "/", - "referrer": "", - "search": "", - "title": "", - "url": "http://localhost/", - }, - "requestId": "12345", - "status": 200, - "url": "http://example.com", - }, - "index": undefined, - "timestamp": , - "type": "network", - } - `) - expect(normalizeUrl).toHaveBeenCalledWith('http://example.com') - }) -}) diff --git a/packages/signals/signals/src/types/analytics-api.ts b/packages/signals/signals/src/types/analytics-api.ts deleted file mode 100644 index 898660209..000000000 --- a/packages/signals/signals/src/types/analytics-api.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Defining stub interface here because we don't want to introduce a prod dependency on the analytics-next package - * Otherwise, we would introduce typescript errors for library consumers (unless they do skipLibCheck: true). - */ - -export type EdgeFnCDNSettings = { - version: number - downloadURL: string -} - -export type AutoInstrumentationCDNSettings = { - sampleRate: number -} - -export interface CDNSettings { - edgeFunction?: EdgeFnCDNSettings | {} - autoInstrumentationSettings?: AutoInstrumentationCDNSettings - [key: string]: unknown -} - -export interface SegmentEventStub { - type: string - context: { - __eventOrigin?: { - type: 'Signal' - } - [key: string]: unknown - } - [key: string]: unknown -} - -export interface SourceMiddlewareParams { - payload: { - obj: SegmentEventStub - } - next: (payload: SourceMiddlewareParams['payload'] | null) => void -} - -export type SourceMiddlewareFunction = ( - middleware: SourceMiddlewareParams -) => null | void | Promise - -export interface DestinationMiddlewareParams { - payload: SourceMiddlewareParams['payload'] - next: SourceMiddlewareParams['next'] - /** - * integration name - * @example "Amplitude (Actions)" - */ - integration: string -} - -export type ID = string | null | undefined - -export interface UserInfo { - id(): ID - anonymousId(): ID -} - -export interface AnyAnalytics { - settings: { - cdnSettings: CDNSettings - writeKey: string - cdnURL?: string // temporarily optional because analytics does not have this API yet - apiHost?: string // temporarily optional because analytics does not have this API yet - } - addSourceMiddleware( - middleware: Function | SourceMiddlewareFunction - ): any | this - track(event: string, properties?: unknown, ...args: any[]): void - identify(...args: any[]): void - page(...args: any[]): void - group(...args: any[]): void - alias(...args: any[]): void - screen(...args: any[]): void - reset(): void - user(): UserInfo - on(name: 'reset', fn: (...args: any[]) => void): void -} - -/** - *CDN Settings Integrations object. - * @example - * { "Fullstory": {...}, "Braze Web Mode (Actions)": {...}} - */ -export interface CDNSettingsIntegrations { - 'Segment.io': any - [integrationName: string]: Record -} - -export type PluginType = 'before' | 'after' | 'destination' - -export interface Plugin> { - name: string - type: PluginType - isLoaded: () => boolean - load: (ctx: Ctx, instance: Analytics) => Promise - - unload?: (ctx: Ctx, instance: Analytics) => Promise | unknown - ready?: () => Promise - track?: (ctx: Ctx) => Promise | Ctx - identify?: (ctx: Ctx) => Promise | Ctx - page?: (ctx: Ctx) => Promise | Ctx - group?: (ctx: Ctx) => Promise | Ctx - alias?: (ctx: Ctx) => Promise | Ctx - screen?: (ctx: Ctx) => Promise | Ctx -} diff --git a/packages/signals/signals/src/types/factories.ts b/packages/signals/signals/src/types/factories.ts deleted file mode 100644 index 45b72aaaf..000000000 --- a/packages/signals/signals/src/types/factories.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - NavigationData, - NavigationSignal, - UserDefinedSignalData, - UserDefinedSignal, - NetworkData, - NetworkSignal, - SignalType, - Signal, - SignalOfType, - InstrumentationSignal, - InteractionData, - InteractionSignal, - SegmentEvent, - EventType, -} from '@segment/analytics-signals-runtime' -import { normalizeUrl } from '../lib/normalize-url' -import { getPageData } from '../lib/page-data' - -type BaseData = Omit< - SignalOfType['data'], - 'page' -> - -/** - * Base Signal Factory - */ -const createBaseSignal = >( - type: Type, - data: Data -) => { - return { - index: undefined, // This will get overridden by a middleware that runs once analytics is instantiated - timestamp: new Date().toISOString(), - anonymousId: '', // to be set by a middleware (that runs once analytics is instantiated) - type, - context: { - library: { - name: '@segment/analytics-next', - version: '0.0.0', - }, - signalsRuntime: '', - }, - data: { - ...data, - page: getPageData(), - }, - } -} - -export const createInstrumentationSignal = ( - rawEvent: SegmentEvent -): InstrumentationSignal => { - return createBaseSignal('instrumentation', { - rawEvent, - type: rawEvent.type as EventType, - }) -} - -export const createInteractionSignal = ( - data: InteractionData -): InteractionSignal => { - return createBaseSignal('interaction', data) -} - -export const createNavigationSignal = ( - data: NavigationData -): NavigationSignal => { - return createBaseSignal('navigation', data) -} - -export const createUserDefinedSignal = ( - data: UserDefinedSignalData -): UserDefinedSignal => { - return createBaseSignal('userDefined', data) -} - -export const createNetworkSignal = (data: NetworkData): NetworkSignal => { - return { - ...createBaseSignal('network', { - ...data, - url: normalizeUrl(data.url), - }), - } -} diff --git a/packages/signals/signals/src/types/index.ts b/packages/signals/signals/src/types/index.ts deleted file mode 100644 index dd8d174be..000000000 --- a/packages/signals/signals/src/types/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './analytics-api' -export * from './process-signal' -export * from './settings' diff --git a/packages/signals/signals/src/types/process-signal.ts b/packages/signals/signals/src/types/process-signal.ts deleted file mode 100644 index 60b551070..000000000 --- a/packages/signals/signals/src/types/process-signal.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Signal, SignalsRuntime } from '@segment/analytics-signals-runtime' - -/** - * Types for the signals runtime - */ -export interface AnalyticsRuntimePublicApi { - track: (...args: any[]) => void - identify: (...args: any[]) => void - alias: (...args: any[]) => void - group: (...args: any[]) => void - page: (...args: any[]) => void - screen: (...args: any[]) => void - reset: () => void -} - -export type ProcessSignalScope = { - analytics: AnalyticsRuntimePublicApi - signals: SignalsRuntime -} - -export interface ProcessSignal { - (signal: Signal, ctx: ProcessSignalScope): void -} diff --git a/packages/signals/signals/src/types/settings.ts b/packages/signals/signals/src/types/settings.ts deleted file mode 100644 index 6d62f348c..000000000 --- a/packages/signals/signals/src/types/settings.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { SignalsMiddleware } from '../core/emitter' -import { ProcessSignal } from './process-signal' - -export interface SignalsPluginSettingsConfig { - /** - * Max number of signals in the default signal store - */ - maxBufferSize?: number - - /** - * Override the default signal processing function from the edge function. If this is set, the edge function will not be used. - */ - processSignal?: string | ProcessSignal - - /** - * Enable debug mode. Same as ?segment_signals_debug=true in the URL - * This sets the log level to 'info', disables redaction, and enables signals ingestion (sending signals to segment) - * @default false - */ - debug?: boolean - - /** - * Disable redaction of signals - * @default false - */ - disableSignalsRedaction?: boolean - - /** - * Enable ingestion of signals - */ - enableSignalsIngestion?: boolean - - /** - * Override signals API host - * @default signals.segment.io/v1 - */ - apiHost?: string - - /** - * Override edge function host - * @default cdn.edgefn.segment.com - */ - functionHost?: string - - /** - * How many signals to flush at once when sending to the signals API - * @default 5 - */ - flushAt?: number - - /** - * How many ms to wait before flushing signals to the API - * @default 2000 - */ - flushInterval?: number - - /** - * Add custom signals middleware - * @example - *```ts - * class MyMiddleware implements SignalsMiddleware { - * process(signal: Signal) { - * // drop the event if some conditions are met - * if ( - * signal.type === 'network' && - * signal.data.action === 'request' && - * ... etc - * ) { - * return null; - * } else { - * return signal; - * } - * } - * const myMiddleware = new MyMiddleware() - * ... - * middleware: [myMiddleware] - * ```` - */ - middleware?: SignalsMiddleware[] | undefined - - /** - * Allow network requests for URLs that match the specified regex. - * Same domain signals will be automatically included, unless `networkSignalsAllowSameDomain` is `false`. - * @example new RegExp("api.foo.com|api.bar.com") // match api.foo.com/bar and api.foo.com/baz and api.bar.com/baz - * @example new RegExp("api.foo.com/bar$") // match api.foo.com/bar but NOT api.foo.com/baz - * @example "api.foo.com" // match api.foo.com/bar and api.foo.com/baz - */ - networkSignalsAllowList?: RegexLike[] | undefined - networkSignalsDisallowList?: RegexLike[] | undefined - - /** - * Allow network requests if originated from the same domain - * @default true - */ - networkSignalsAllowSameDomain?: boolean | undefined - - /** - * Custom signal storage implementation - */ - signalStorage?: SignalStorage | undefined - - /** - * Choose between sessionStorage and indexDB. - * IndexDB is more performant and has no size requirements, but has the security caveat that signals can be retained across sessions (cleared on new session, but still technically accessible), and only cleared at the beginning of a new session. - * SessionStorage is cleared on tab close. - * - * @default 'indexDB' - */ - signalStorageType?: 'session' | 'indexDB' | undefined - - /** - * Custom selectors that map to components that should be observed for attribute changes (if the default list is not sufficient) - * @example [`[id="bar"]`, `.foo`] - */ - mutationGenExtraSelectors?: string[] - /** - * @example - * // add a role to the roles - * (defaultRoles) => [...defaultRoles, 'grid'] - * // remove a role from the roles - * (defaultRoles) => defaultRoles.filter(role => role !== 'grid') - */ - mutationGenObservedRoles?: (defaultRoles: string[]) => string[] - /** - * @example - * // add a new tag to the tags - * (defaultTags) => [...defaultTags, 'video'] - * // remove a tag from the tags - * (defaultTags) => defaultTags.filter(tag => tag.toLowerCase() !== 'video') - */ - mutationGenObservedTags?: (defaultTags: string[]) => string[] - /** - * How often to poll the DOM for new elements to observe (ms) - * @default 400 - */ - mutationGenPollInterval?: number - /** - * - * Which attributes to observe for changes on the observed elements. This is used for MutationObserver. - * @example - * // add a new attribute to watch for changes - * (defaultAttributes) => [...defaultAttributes, 'aria-label'] - * // remove an attribute from the list of attributes to watch for changes - * (defaultAttributes) => defaultAttributes.filter(attr => attr.toLowerCase() !== 'aria-selected') - */ - mutationGenObservedAttributes?: (defaultAttributes: string[]) => string[] - - /** - * What sandbox strategy to use - * - global - [EXPERIMENTAL] evaluate everything in the global scope -- use this if you want to avoid CSP errors. - * - iframe - use a web worker and regular evaluation - * @default 'iframe' - */ - sandboxStrategy?: 'iframe' | 'global' -} - -export type RegexLike = RegExp | string - -export interface SignalStorage { - getAll(): Promise | Signal[] - add(value: Signal): Promise | void - clear(): void -} diff --git a/packages/signals/signals/src/utils/index.ts b/packages/signals/signals/src/utils/index.ts deleted file mode 100644 index 149a4c361..000000000 --- a/packages/signals/signals/src/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ts-helpers' diff --git a/packages/signals/signals/src/utils/is-class.ts b/packages/signals/signals/src/utils/is-class.ts deleted file mode 100644 index 0cd8de761..000000000 --- a/packages/signals/signals/src/utils/is-class.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const isClass = (value: any): value is NewableFunction => { - return ( - typeof value === 'function' && value.prototype.constructor !== undefined - ) -} diff --git a/packages/signals/signals/src/utils/ts-helpers.ts b/packages/signals/signals/src/utils/ts-helpers.ts deleted file mode 100644 index 0daaab76f..000000000 --- a/packages/signals/signals/src/utils/ts-helpers.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Resolve a type to its final form. - */ -type Compute = { [K in keyof T]: T[K] } & {} - -/** - * A utility type that makes a specified set of fields optional in a given type. - * @template T - The type to make fields optional in. - * @template K - The keys of the fields to make optional. - * @example - * type MyType = { - * a: string - * b: number - * } - * type MyTypeWithOptionalA = OptionalField - * // MyTypeWithOptionalA is equivalent to: - * type MyTypeWithOptionalA = { - * a?: string - * b: number - * } - */ -export type OptionalField = Compute< - Omit & Partial> -> - -/** - * Helper function for exhaustive checks of discriminated unions. - */ -export function assertNever(value: never): never { - throw new Error( - `Unhandled discriminated union member: ${JSON.stringify(value)}` - ) -} - -export type EmptyObject = Record diff --git a/packages/signals/signals/tsconfig.build.json b/packages/signals/signals/tsconfig.build.json deleted file mode 100644 index 6b091bfd8..000000000 --- a/packages/signals/signals/tsconfig.build.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": ["src"], - "exclude": ["**/__tests__/**", "**/test-helpers/**"], - "compilerOptions": { - "noEmit": false, - "outDir": "./dist/esm", - "declarationDir": "./dist/types" - } -} diff --git a/packages/signals/signals/tsconfig.json b/packages/signals/signals/tsconfig.json deleted file mode 100644 index 963a8bc52..000000000 --- a/packages/signals/signals/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "exclude": ["node_modules", "dist"], - "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/webpack.config.js b/packages/signals/signals/webpack.config.js deleted file mode 100644 index 0c3cd92a7..000000000 --- a/packages/signals/signals/webpack.config.js +++ /dev/null @@ -1,31 +0,0 @@ -const common = require('@internal/config-webpack/webpack.config.common') -const path = require('path') -const { merge } = require('webpack-merge') -const isProd = process.env.NODE_ENV === 'production' -/** - * @type { import('webpack').Configuration } - */ -module.exports = merge(common, { - entry: { - 'analytics-signals.umd': { - import: path.resolve(__dirname, 'src/index.umd.ts'), - library: { - name: 'AnalyticsBrowserSignals', - type: 'umd', - }, - }, - 'analytics-signals.global': { - import: path.resolve(__dirname, 'src/index.ts'), - library: { - type: 'window', - }, - }, - }, - output: { - filename: isProd ? '[name].js' : '[name].development.js', - path: path.resolve(__dirname, 'dist/umd'), - chunkFilename: isProd - ? '[name].[contenthash].js' - : '[name].chunk.development.js', - }, -}) diff --git a/playgrounds/standalone-playground/package.json b/playgrounds/standalone-playground/package.json index 45a168240..7ed8609b7 100644 --- a/playgrounds/standalone-playground/package.json +++ b/playgrounds/standalone-playground/package.json @@ -10,13 +10,11 @@ "dev": "yarn concurrently 'yarn start' 'yarn watch'", "watch": "yarn concurrently 'yarn:watch:*'", "watch:ajs": "ASSET_PATH='/node_modules/@segment/analytics-next/dist/umd/' yarn workspace @segment/analytics-next watch", - "watch:signals": "yarn workspace @segment/analytics-signals watch", "concurrently": "yarn run -T concurrently" }, "dependencies": { "@segment/analytics-consent-wrapper-onetrust": "workspace:^", - "@segment/analytics-next": "workspace:^", - "@segment/analytics-signals": "workspace:^" + "@segment/analytics-next": "workspace:^" }, "devDependencies": { "http-server": "14.1.1" diff --git a/playgrounds/standalone-playground/pages/index-signals.html b/playgrounds/standalone-playground/pages/index-signals.html deleted file mode 100644 index 48c05d39e..000000000 --- a/playgrounds/standalone-playground/pages/index-signals.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - - -
- - -
- - - - - - -
- -
- - -
-
- -

-  

-
-  
-
-
-
\ No newline at end of file
diff --git a/scripts/package.json b/scripts/package.json
index c04a50d3a..7d3c6a118 100644
--- a/scripts/package.json
+++ b/scripts/package.json
@@ -7,7 +7,6 @@
     "lint": "yarn concurrently 'yarn:eslint .' 'yarn:tsc --noEmit'",
     "create-release-from-tags": "yarn ts-node-script --files create-release-from-tags/run.ts",
     "purge-cdn-cache": "yarn concurrently 'yarn:purge-cdn-cache:*'",
-    "purge-cdn-cache:signals": "node purge-cdn-cache.js '@segment/analytics-signals' 'packages/signals/signals/dist/umd'",
     "purge-cdn-cache:consent": "node purge-cdn-cache.js '@segment/analytics-consent-wrapper-onetrust' 'packages/consent/consent-wrapper-onetrust/dist/umd'",
     "test": "yarn jest",
     "tsc": "yarn run -T tsc",

From 9bb21b1e4eedc0105e03ffd7dd703b1c98808fd2 Mon Sep 17 00:00:00 2001
From: Seth Silesky <5115498+silesky@users.noreply.github.com>
Date: Mon, 14 Jul 2025 21:23:33 -0500
Subject: [PATCH 2/2] update yarn.lock

---
 yarn.lock | 2854 +++--------------------------------------------------
 1 file changed, 157 insertions(+), 2697 deletions(-)

diff --git a/yarn.lock b/yarn.lock
index e55df780a..debb23141 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -243,15 +243,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/helper-annotate-as-pure@npm:^7.24.6":
-  version: 7.24.6
-  resolution: "@babel/helper-annotate-as-pure@npm:7.24.6"
-  dependencies:
-    "@babel/types": ^7.24.6
-  checksum: 9ddcc2ddfa64213311d71bead56ecccdadca5455dc54528c545a2efc1d8010fb7327aef2d90ac7e71b0d0becfed0ffb00553b1e192ff00596efe4161511891cf
-  languageName: node
-  linkType: hard
-
 "@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.22.5":
   version: 7.22.10
   resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.22.10"
@@ -453,15 +444,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/helper-module-imports@npm:^7.24.6":
-  version: 7.24.6
-  resolution: "@babel/helper-module-imports@npm:7.24.6"
-  dependencies:
-    "@babel/types": ^7.24.6
-  checksum: 3484420c45529aac34cb14111a03c78edab84e5c4419634affe61176d832af82963395ea319f67c7235fd4106d9052a9f3ce012d2d57d56644572d3f7d495231
-  languageName: node
-  linkType: hard
-
 "@babel/helper-module-transforms@npm:^7.18.0":
   version: 7.18.0
   resolution: "@babel/helper-module-transforms@npm:7.18.0"
@@ -538,13 +520,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/helper-plugin-utils@npm:^7.24.6":
-  version: 7.24.6
-  resolution: "@babel/helper-plugin-utils@npm:7.24.6"
-  checksum: d22bb82c75afed0d8c37784876fd6deb9db06ef21526db909ef7986a6050b50beb60a7823c08a1bb7c57c668af2e086d8086e88b6f9140b0d9ade07472f7c748
-  languageName: node
-  linkType: hard
-
 "@babel/helper-remap-async-to-generator@npm:^7.22.5, @babel/helper-remap-async-to-generator@npm:^7.22.9":
   version: 7.22.9
   resolution: "@babel/helper-remap-async-to-generator@npm:7.22.9"
@@ -630,13 +605,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/helper-string-parser@npm:^7.24.6":
-  version: 7.24.6
-  resolution: "@babel/helper-string-parser@npm:7.24.6"
-  checksum: c8c614a663928b67c5c65cfea958ed20c858fa2af8c957d301bd852c0ab98adae0861f081fd8f5add16539d9393bd4b10b8c86a97a9d7304f70a6a67b2c2ff07
-  languageName: node
-  linkType: hard
-
 "@babel/helper-validator-identifier@npm:^7.10.4, @babel/helper-validator-identifier@npm:^7.14.5":
   version: 7.14.8
   resolution: "@babel/helper-validator-identifier@npm:7.14.8"
@@ -672,13 +640,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/helper-validator-identifier@npm:^7.24.6":
-  version: 7.24.6
-  resolution: "@babel/helper-validator-identifier@npm:7.24.6"
-  checksum: a265a6fba570332dca63ad7e749b867d29b52da2573dc62bf19b5b8c5387d4f4296af33da9da7c71ffe3d3abecd743418278f56d38b057ad4b53f09b937fe113
-  languageName: node
-  linkType: hard
-
 "@babel/helper-validator-option@npm:^7.16.7":
   version: 7.16.7
   resolution: "@babel/helper-validator-option@npm:7.16.7"
@@ -700,13 +661,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/helper-validator-option@npm:^7.24.6":
-  version: 7.24.6
-  resolution: "@babel/helper-validator-option@npm:7.24.6"
-  checksum: 5defb2da74e1cac9497016f4e41698aeed75ec7a5e9dc07e777cdb67ef73cd2e27bd2bf8a3ab8d37e0b93a6a45524a9728f03e263afdef452436cf74794bde87
-  languageName: node
-  linkType: hard
-
 "@babel/helper-wrap-function@npm:^7.22.9":
   version: 7.22.10
   resolution: "@babel/helper-wrap-function@npm:7.22.10"
@@ -1027,17 +981,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/plugin-syntax-jsx@npm:^7.24.6":
-  version: 7.24.6
-  resolution: "@babel/plugin-syntax-jsx@npm:7.24.6"
-  dependencies:
-    "@babel/helper-plugin-utils": ^7.24.6
-  peerDependencies:
-    "@babel/core": ^7.0.0-0
-  checksum: e288681cab57d059b0b2e132040eb5e21a158c40229c600e77cb0289ba5d32a2102af94e43390d270e0ddd968685e9de8d10dab0291c53b84e2219a7bc4cdb54
-  languageName: node
-  linkType: hard
-
 "@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4, @babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3":
   version: 7.10.4
   resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4"
@@ -1625,17 +1568,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/plugin-transform-react-display-name@npm:^7.24.6":
-  version: 7.24.6
-  resolution: "@babel/plugin-transform-react-display-name@npm:7.24.6"
-  dependencies:
-    "@babel/helper-plugin-utils": ^7.24.6
-  peerDependencies:
-    "@babel/core": ^7.0.0-0
-  checksum: 23e5992815b4f9bda6b90673d94ba74d001281fa2b02780d59f666234ed9b05782546364b13009fe0d72b3d5b829f2c6831948b0af120294f5f2e5f4da9c7eab
-  languageName: node
-  linkType: hard
-
 "@babel/plugin-transform-react-jsx-development@npm:^7.16.7":
   version: 7.22.5
   resolution: "@babel/plugin-transform-react-jsx-development@npm:7.22.5"
@@ -1647,17 +1579,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/plugin-transform-react-jsx-development@npm:^7.24.6":
-  version: 7.24.6
-  resolution: "@babel/plugin-transform-react-jsx-development@npm:7.24.6"
-  dependencies:
-    "@babel/plugin-transform-react-jsx": ^7.24.6
-  peerDependencies:
-    "@babel/core": ^7.0.0-0
-  checksum: 912993aa8546d3aa129d6a567018c29bc99f0a3c9a99062f756d48c8ad0444f42314a245a1434964e23685a59c4a1564abb8e9a251d8ca216ed726661ede50d9
-  languageName: node
-  linkType: hard
-
 "@babel/plugin-transform-react-jsx-self@npm:^7.16.7":
   version: 7.23.3
   resolution: "@babel/plugin-transform-react-jsx-self@npm:7.23.3"
@@ -1695,33 +1616,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/plugin-transform-react-jsx@npm:^7.24.6":
-  version: 7.24.6
-  resolution: "@babel/plugin-transform-react-jsx@npm:7.24.6"
-  dependencies:
-    "@babel/helper-annotate-as-pure": ^7.24.6
-    "@babel/helper-module-imports": ^7.24.6
-    "@babel/helper-plugin-utils": ^7.24.6
-    "@babel/plugin-syntax-jsx": ^7.24.6
-    "@babel/types": ^7.24.6
-  peerDependencies:
-    "@babel/core": ^7.0.0-0
-  checksum: 6e6ef2a9c364c81dc865cfac12fb45075470e918af648239b3ef9d4720577762950b9db6db2d1f8c2e4f3f0c2e4e169d4ebc7c5c2037fc0755ee606016a413f0
-  languageName: node
-  linkType: hard
-
-"@babel/plugin-transform-react-pure-annotations@npm:^7.24.6":
-  version: 7.24.6
-  resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.24.6"
-  dependencies:
-    "@babel/helper-annotate-as-pure": ^7.24.6
-    "@babel/helper-plugin-utils": ^7.24.6
-  peerDependencies:
-    "@babel/core": ^7.0.0-0
-  checksum: e358ecabb328a952d8aa36acbbf7bf017b1905d97401eaafb6b99572d8d4b477a3e37cccc1cd582d980952936b7e6dd370ade488e167591c1c9fb7940141c301
-  languageName: node
-  linkType: hard
-
 "@babel/plugin-transform-regenerator@npm:^7.22.10":
   version: 7.22.10
   resolution: "@babel/plugin-transform-regenerator@npm:7.22.10"
@@ -1965,22 +1859,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/preset-react@npm:^7.24.6":
-  version: 7.24.6
-  resolution: "@babel/preset-react@npm:7.24.6"
-  dependencies:
-    "@babel/helper-plugin-utils": ^7.24.6
-    "@babel/helper-validator-option": ^7.24.6
-    "@babel/plugin-transform-react-display-name": ^7.24.6
-    "@babel/plugin-transform-react-jsx": ^7.24.6
-    "@babel/plugin-transform-react-jsx-development": ^7.24.6
-    "@babel/plugin-transform-react-pure-annotations": ^7.24.6
-  peerDependencies:
-    "@babel/core": ^7.0.0-0
-  checksum: 3bf120b3c29521f1e9f7d5cea864ea3cf411b385bc32f516b5956e209cca074f05c32a1c2baa8b24f3100c7868832f9a37d6f3ad6989aec3592bc09ab90e991a
-  languageName: node
-  linkType: hard
-
 "@babel/preset-typescript@npm:^7.22.11":
   version: 7.22.11
   resolution: "@babel/preset-typescript@npm:7.22.11"
@@ -2182,17 +2060,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@babel/types@npm:^7.24.6":
-  version: 7.24.6
-  resolution: "@babel/types@npm:7.24.6"
-  dependencies:
-    "@babel/helper-string-parser": ^7.24.6
-    "@babel/helper-validator-identifier": ^7.24.6
-    to-fast-properties: ^2.0.0
-  checksum: 58d798dd37e6b14f818730b4536795d68d28ccd5dc2a105fd977104789b20602be11d92cdd47cdbd48d8cce3cc0e14c7773813357ad9d5d6e94d70587eb45bf5
-  languageName: node
-  linkType: hard
-
 "@bcoe/v8-coverage@npm:^0.2.3":
   version: 0.2.3
   resolution: "@bcoe/v8-coverage@npm:0.2.3"
@@ -2945,57 +2812,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@formatjs/ecma402-abstract@npm:2.3.1":
-  version: 2.3.1
-  resolution: "@formatjs/ecma402-abstract@npm:2.3.1"
-  dependencies:
-    "@formatjs/fast-memoize": 2.2.5
-    "@formatjs/intl-localematcher": 0.5.9
-    decimal.js: 10
-    tslib: 2
-  checksum: d8f521d696294abe4ca3067fa1fa7338903a3e3dd6e7da4d7ea58d221fd061b661e5406c6a5c531bb887029457e9578cf871e7a47f999bbdeccbf56614e9ce38
-  languageName: node
-  linkType: hard
-
-"@formatjs/fast-memoize@npm:2.2.5":
-  version: 2.2.5
-  resolution: "@formatjs/fast-memoize@npm:2.2.5"
-  dependencies:
-    tslib: 2
-  checksum: 1009b9f782c7b8c0530cde86a68d174bc8055bbf3207eb662429016c2f793bef534e950a13d012861826860ec97d0240fd89eda619267a8191270f581c025f1b
-  languageName: node
-  linkType: hard
-
-"@formatjs/icu-messageformat-parser@npm:2.9.7":
-  version: 2.9.7
-  resolution: "@formatjs/icu-messageformat-parser@npm:2.9.7"
-  dependencies:
-    "@formatjs/ecma402-abstract": 2.3.1
-    "@formatjs/icu-skeleton-parser": 1.8.11
-    tslib: 2
-  checksum: d388f45780d505c6085a7a6eb2a9d2a5e10625a596ba37e1086625026af268fd34c6f2e0607785405a45e51e8e51e5d41d6b9832fff01716d8ba6576c16c6ea1
-  languageName: node
-  linkType: hard
-
-"@formatjs/icu-skeleton-parser@npm:1.8.11":
-  version: 1.8.11
-  resolution: "@formatjs/icu-skeleton-parser@npm:1.8.11"
-  dependencies:
-    "@formatjs/ecma402-abstract": 2.3.1
-    tslib: 2
-  checksum: f3d507e56754200431fdcc2d573a4075ed886a9dd5e3e97cdbb7af7bdf44d9101cea4956f23cdb2d5b9dbfeba20fdd40c8286508ad1a4fb5a6f730234562d484
-  languageName: node
-  linkType: hard
-
-"@formatjs/intl-localematcher@npm:0.5.9":
-  version: 0.5.9
-  resolution: "@formatjs/intl-localematcher@npm:0.5.9"
-  dependencies:
-    tslib: 2
-  checksum: b9f5aaa2cf42b478163853413edee9e13022b04ac97c6805fbd7591cd8a424d4bb357ed7740dd7149b7c502b2182e28dce986967121ccc721e74f6ff58807c79
-  languageName: node
-  linkType: hard
-
 "@gar/promisify@npm:^1.1.3":
   version: 1.1.3
   resolution: "@gar/promisify@npm:1.1.3"
@@ -3138,56 +2954,6 @@ __metadata:
   languageName: unknown
   linkType: soft
 
-"@internal/signals-example@workspace:packages/signals/signals-example":
-  version: 0.0.0-use.local
-  resolution: "@internal/signals-example@workspace:packages/signals/signals-example"
-  dependencies:
-    "@babel/core": ^7.22.11
-    "@babel/preset-env": ^7.22.10
-    "@babel/preset-react": ^7.24.6
-    "@babel/preset-typescript": ^7.22.11
-    "@segment/analytics-next": "workspace:^"
-    "@segment/analytics-signals": "workspace:^"
-    "@types/react": ^18.0.0
-    "@types/react-dom": ^18
-    babel-loader: ^8.0.0
-    css-loader: ^7.1.2
-    dotenv-webpack: ^8.1.0
-    html-webpack-plugin: ^5.6.0
-    react: ^18.0.0
-    react-dom: ^18.0.0
-    react-hook-form: ^7.54.2
-    react-router-dom: ^6.23.1
-    style-loader: ^4.0.0
-    typescript: ^4.7.0
-    webpack: ^5.94.0
-    webpack-cli: ^4.8.0
-    webpack-dev-server: ^4.15.1
-  languageName: unknown
-  linkType: soft
-
-"@internal/signals-integration-tests@workspace:packages/signals/signals-integration-tests":
-  version: 0.0.0-use.local
-  resolution: "@internal/signals-integration-tests@workspace:packages/signals/signals-integration-tests"
-  dependencies:
-    "@internal/config": "workspace:^"
-    "@internal/test-helpers": "workspace:^"
-    "@playwright/test": ^1.28.1
-    "@segment/analytics-next": "workspace:^"
-    "@segment/analytics-signals": "workspace:^"
-    "@types/react": ^18.0.0
-    "@types/react-dom": ^18
-    globby: ^11.0.2
-    http-server: 14.1.1
-    react: ^18.0.0
-    react-aria-components: ^1.5.0
-    react-dom: ^18.0.0
-    tslib: ^2.4.1
-    webpack: ^5.94.0
-    webpack-cli: ^4.8.0
-  languageName: unknown
-  linkType: soft
-
 "@internal/test-helpers@workspace:^, @internal/test-helpers@workspace:packages/test-helpers":
   version: 0.0.0-use.local
   resolution: "@internal/test-helpers@workspace:packages/test-helpers"
@@ -3197,43 +2963,6 @@ __metadata:
   languageName: unknown
   linkType: soft
 
-"@internationalized/date@npm:^3.6.0":
-  version: 3.6.0
-  resolution: "@internationalized/date@npm:3.6.0"
-  dependencies:
-    "@swc/helpers": ^0.5.0
-  checksum: 82a66c7d7eef8bc49c4ee5e99ecfa91a4752a3a96296a34c5549fe3fb98c5d37c3688887a253ffb991749d3425f7045c7c6b24c4f98c4929d0ef7f8312fa68ec
-  languageName: node
-  linkType: hard
-
-"@internationalized/message@npm:^3.1.6":
-  version: 3.1.6
-  resolution: "@internationalized/message@npm:3.1.6"
-  dependencies:
-    "@swc/helpers": ^0.5.0
-    intl-messageformat: ^10.1.0
-  checksum: a291d32e797a3694d1279c4fb74f2812991f007b15fbd67e148d2089339a4f3e11b4803eae6f1cc4ae1a1872b39bdcafe30f9bb365accdf5ed2af063e532d00f
-  languageName: node
-  linkType: hard
-
-"@internationalized/number@npm:^3.6.0":
-  version: 3.6.0
-  resolution: "@internationalized/number@npm:3.6.0"
-  dependencies:
-    "@swc/helpers": ^0.5.0
-  checksum: 764078650ac562a54a22938d6889ed2cb54e411a4c58b098dabc8514572709bbc206f8e44b50bd684600e454b0276c2617ddc6d9a7345521f2896a13b1c085a7
-  languageName: node
-  linkType: hard
-
-"@internationalized/string@npm:^3.2.5":
-  version: 3.2.5
-  resolution: "@internationalized/string@npm:3.2.5"
-  dependencies:
-    "@swc/helpers": ^0.5.0
-  checksum: e1ad90f418e8a35f49b6fe91cc91ea5230083808b337feaff60f8a0a8a32ee33895728bc4024cdfe93bf6596b3a3dc72cd5f8b7daba29962fbc68827c816fecd
-  languageName: node
-  linkType: hard
-
 "@isaacs/cliui@npm:^8.0.2":
   version: 8.0.2
   resolution: "@isaacs/cliui@npm:8.0.2"
@@ -4001,1780 +3730,203 @@ __metadata:
 "@next/swc-linux-x64-gnu@npm:12.3.4":
   version: 12.3.4
   resolution: "@next/swc-linux-x64-gnu@npm:12.3.4"
-  conditions: os=linux & cpu=x64 & libc=glibc
-  languageName: node
-  linkType: hard
-
-"@next/swc-linux-x64-musl@npm:12.3.4":
-  version: 12.3.4
-  resolution: "@next/swc-linux-x64-musl@npm:12.3.4"
-  conditions: os=linux & cpu=x64 & libc=musl
-  languageName: node
-  linkType: hard
-
-"@next/swc-win32-arm64-msvc@npm:12.3.4":
-  version: 12.3.4
-  resolution: "@next/swc-win32-arm64-msvc@npm:12.3.4"
-  conditions: os=win32 & cpu=arm64
-  languageName: node
-  linkType: hard
-
-"@next/swc-win32-ia32-msvc@npm:12.3.4":
-  version: 12.3.4
-  resolution: "@next/swc-win32-ia32-msvc@npm:12.3.4"
-  conditions: os=win32 & cpu=ia32
-  languageName: node
-  linkType: hard
-
-"@next/swc-win32-x64-msvc@npm:12.3.4":
-  version: 12.3.4
-  resolution: "@next/swc-win32-x64-msvc@npm:12.3.4"
-  conditions: os=win32 & cpu=x64
-  languageName: node
-  linkType: hard
-
-"@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3":
-  version: 2.1.8-no-fsevents.3
-  resolution: "@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3"
-  checksum: ee55cc9241aeea7eb94b8a8551bfa4246c56c53bc71ecda0a2104018fcc328ba5723b33686bdf9cc65d4df4ae65e8016b89e0bbdeb94e0309fe91bb9ced42344
-  languageName: node
-  linkType: hard
-
-"@node-kit/extra.fs@npm:3.2.0":
-  version: 3.2.0
-  resolution: "@node-kit/extra.fs@npm:3.2.0"
-  checksum: 48781d37ddd45f544774c17fccf31e1bfe648a16354cf8b20b28f0315798d977336a50c2a4cbb421fd9016792a0860cb2254e7450885324e7ace08903176b58b
-  languageName: node
-  linkType: hard
-
-"@node-kit/yarn-workspace-root@npm:^3.2.0":
-  version: 3.2.0
-  resolution: "@node-kit/yarn-workspace-root@npm:3.2.0"
-  dependencies:
-    "@node-kit/extra.fs": 3.2.0
-    find-up: ^5.0.0
-    micromatch: ^4.0.5
-  checksum: 18eca9649017f1b419a230909c319d57fe26400d3074685bb89946be30b3eb6670594dc7bb20d1a4d83cb4b991acf9818026b214fb879717f5ca0290ed934c3e
-  languageName: node
-  linkType: hard
-
-"@nodelib/fs.scandir@npm:2.1.5":
-  version: 2.1.5
-  resolution: "@nodelib/fs.scandir@npm:2.1.5"
-  dependencies:
-    "@nodelib/fs.stat": 2.0.5
-    run-parallel: ^1.1.9
-  checksum: a970d595bd23c66c880e0ef1817791432dbb7acbb8d44b7e7d0e7a22f4521260d4a83f7f9fd61d44fda4610105577f8f58a60718105fb38352baed612fd79e59
-  languageName: node
-  linkType: hard
-
-"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2":
-  version: 2.0.5
-  resolution: "@nodelib/fs.stat@npm:2.0.5"
-  checksum: 012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0
-  languageName: node
-  linkType: hard
-
-"@nodelib/fs.walk@npm:^1.2.3":
-  version: 1.2.8
-  resolution: "@nodelib/fs.walk@npm:1.2.8"
-  dependencies:
-    "@nodelib/fs.scandir": 2.1.5
-    fastq: ^1.6.0
-  checksum: 190c643f156d8f8f277bf2a6078af1ffde1fd43f498f187c2db24d35b4b4b5785c02c7dc52e356497b9a1b65b13edc996de08de0b961c32844364da02986dc53
-  languageName: node
-  linkType: hard
-
-"@npmcli/fs@npm:^2.1.0":
-  version: 2.1.0
-  resolution: "@npmcli/fs@npm:2.1.0"
-  dependencies:
-    "@gar/promisify": ^1.1.3
-    semver: ^7.3.5
-  checksum: 6ec6d678af6da49f9dac50cd882d7f661934dd278972ffbaacde40d9eaa2871292d634000a0cca9510f6fc29855fbd4af433e1adbff90a524ec3eaf140f1219b
-  languageName: node
-  linkType: hard
-
-"@npmcli/move-file@npm:^2.0.0":
-  version: 2.0.0
-  resolution: "@npmcli/move-file@npm:2.0.0"
-  dependencies:
-    mkdirp: ^1.0.4
-    rimraf: ^3.0.2
-  checksum: 1388777b507b0c592d53f41b9d182e1a8de7763bc625fc07999b8edbc22325f074e5b3ec90af79c89d6987fdb2325bc66d59f483258543c14a43661621f841b0
-  languageName: node
-  linkType: hard
-
-"@npmcli/promise-spawn@npm:^7.0.0":
-  version: 7.0.0
-  resolution: "@npmcli/promise-spawn@npm:7.0.0"
-  dependencies:
-    which: ^4.0.0
-  checksum: 22a8c4fd4ef2729cf75d13b0b294e8c695e08bdb2143e951288056656091fc5281e8baf330c97a6bc803e6fc09489028bf80dcd787972597ef9fda9a9349fc0f
-  languageName: node
-  linkType: hard
-
-"@pkgjs/parseargs@npm:^0.11.0":
-  version: 0.11.0
-  resolution: "@pkgjs/parseargs@npm:0.11.0"
-  checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f
-  languageName: node
-  linkType: hard
-
-"@playground/next-playground@workspace:playgrounds/next-playground":
-  version: 0.0.0-use.local
-  resolution: "@playground/next-playground@workspace:playgrounds/next-playground"
-  dependencies:
-    "@builder.io/partytown": ^0.7.4
-    "@next/bundle-analyzer": ^12.1.5
-    "@segment/analytics-next": "workspace:^"
-    "@types/faker": ^5.1.2
-    "@types/react": ^17.0.37
-    eslint-config-next: ^12.1.6
-    faker: ^5.1.0
-    lodash: ^4.17.21
-    next: ^12.1.0
-    prismjs: ^1.27.0
-    rc-table: ^7.10.0
-    react: ^17.0.2
-    react-dom: ^17.0.2
-    react-json-tree: ^0.13.0
-    react-simple-code-editor: ^0.11.0
-    source-map-loader: ^3.0.1
-  languageName: unknown
-  linkType: soft
-
-"@playground/standalone-playground@workspace:playgrounds/standalone-playground":
-  version: 0.0.0-use.local
-  resolution: "@playground/standalone-playground@workspace:playgrounds/standalone-playground"
-  dependencies:
-    "@segment/analytics-consent-wrapper-onetrust": "workspace:^"
-    "@segment/analytics-next": "workspace:^"
-    "@segment/analytics-signals": "workspace:^"
-    http-server: 14.1.1
-  languageName: unknown
-  linkType: soft
-
-"@playground/with-vite@workspace:playgrounds/with-vite":
-  version: 0.0.0-use.local
-  resolution: "@playground/with-vite@workspace:playgrounds/with-vite"
-  dependencies:
-    "@segment/analytics-next": "workspace:^"
-    "@types/react": ^18
-    "@types/react-dom": ^18
-    "@vitejs/plugin-react": ^1.3.0
-    react: ^18.0.0
-    react-dom: ^18.0.0
-    typescript: ^4.7.0
-    vite: ^2.9.18
-  languageName: unknown
-  linkType: soft
-
-"@playwright/test@npm:^1.28.1":
-  version: 1.28.1
-  resolution: "@playwright/test@npm:1.28.1"
-  dependencies:
-    "@types/node": "*"
-    playwright-core: 1.28.1
-  bin:
-    playwright: cli.js
-  checksum: dc39dfdf848171a6c65fc32a9dbc95162684a4a1e3401dd157d7d6822a065d8dcb96b2484fc3b223baea4da774450fddaeaa6d4d21546d17d45f01884fa8d7c5
-  languageName: node
-  linkType: hard
-
-"@polka/url@npm:^1.0.0-next.9":
-  version: 1.0.0-next.11
-  resolution: "@polka/url@npm:1.0.0-next.11"
-  checksum: db1626fb6d7167ce2de6223c95f0a5ff8e1e7c56b2e8709f904f219d8fcc7b075de842ea8bf0ed7af9f5bc350b166b286b241636982f10d0f02964f34215a0e0
-  languageName: node
-  linkType: hard
-
-"@rc-component/context@npm:^1.4.0":
-  version: 1.4.0
-  resolution: "@rc-component/context@npm:1.4.0"
-  dependencies:
-    "@babel/runtime": ^7.10.1
-    rc-util: ^5.27.0
-  peerDependencies:
-    react: ">=16.9.0"
-    react-dom: ">=16.9.0"
-  checksum: 3771237de1e82a453cfff7b5f0ca0dcc370a2838be8ecbfe172c26dec2e94dc2354a8b3061deaff7e633e418fc1b70ce3d10d770603f12dc477fe03f2ada7059
-  languageName: node
-  linkType: hard
-
-"@react-aria/breadcrumbs@npm:^3.5.19":
-  version: 3.5.19
-  resolution: "@react-aria/breadcrumbs@npm:3.5.19"
-  dependencies:
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/link": ^3.7.7
-    "@react-aria/utils": ^3.26.0
-    "@react-types/breadcrumbs": ^3.7.9
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 2f0ceeeea5a4bf248739d574fd163ce09161f16dfdbf9a7bc5f55b7ac3bdda76bf9e6add8f085b7b70ea2bfd47840c950e03700299eb3755e3ff12724213f6b7
-  languageName: node
-  linkType: hard
-
-"@react-aria/button@npm:^3.11.0":
-  version: 3.11.0
-  resolution: "@react-aria/button@npm:3.11.0"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/toolbar": 3.0.0-beta.11
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/toggle": ^3.8.0
-    "@react-types/button": ^3.10.1
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 4bb57fef1be1834cbcdad13e9b4942f888392ab6806dcb904ca72f7bbc0d35be83e286e4af9301b916802372c19521880883f5ee61d4becad67edd9c7c76bea2
-  languageName: node
-  linkType: hard
-
-"@react-aria/calendar@npm:^3.6.0":
-  version: 3.6.0
-  resolution: "@react-aria/calendar@npm:3.6.0"
-  dependencies:
-    "@internationalized/date": ^3.6.0
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/live-announcer": ^3.4.1
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/calendar": ^3.6.0
-    "@react-types/button": ^3.10.1
-    "@react-types/calendar": ^3.5.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: b7cba0c72f60cec776495820b32a99c8dff29b5cf58719c4c1379a53e58d637f95dccc647cce1b648ec8cfbca6ca4f68f1b1c0d861cd665c3bbdd93dbc60d5cc
-  languageName: node
-  linkType: hard
-
-"@react-aria/checkbox@npm:^3.15.0":
-  version: 3.15.0
-  resolution: "@react-aria/checkbox@npm:3.15.0"
-  dependencies:
-    "@react-aria/form": ^3.0.11
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/label": ^3.7.13
-    "@react-aria/toggle": ^3.10.10
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/checkbox": ^3.6.10
-    "@react-stately/form": ^3.1.0
-    "@react-stately/toggle": ^3.8.0
-    "@react-types/checkbox": ^3.9.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: edfeae6a037b6432c3a9d3fc56299df965314f2db3ad3fb652ec13ea3fd3a738c0b153dfec19feb52a5540a4255971815124c20aed10fd67067702a4301ef750
-  languageName: node
-  linkType: hard
-
-"@react-aria/collections@npm:3.0.0-alpha.6":
-  version: 3.0.0-alpha.6
-  resolution: "@react-aria/collections@npm:3.0.0-alpha.6"
-  dependencies:
-    "@react-aria/ssr": ^3.9.7
-    "@react-aria/utils": ^3.26.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-    use-sync-external-store: ^1.2.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 9495de236b0a9cf1c925a3d39e641ede78d53a394fb9bf75063fece2399d869d36225d7aad8940caac2629f6e388afc963f83e72bd1fbc6466a4464965ca0e12
-  languageName: node
-  linkType: hard
-
-"@react-aria/color@npm:^3.0.2":
-  version: 3.0.2
-  resolution: "@react-aria/color@npm:3.0.2"
-  dependencies:
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/numberfield": ^3.11.9
-    "@react-aria/slider": ^3.7.14
-    "@react-aria/spinbutton": ^3.6.10
-    "@react-aria/textfield": ^3.15.0
-    "@react-aria/utils": ^3.26.0
-    "@react-aria/visually-hidden": ^3.8.18
-    "@react-stately/color": ^3.8.1
-    "@react-stately/form": ^3.1.0
-    "@react-types/color": ^3.0.1
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: a7ed07e8fca53ebabcabe4c193fde3ffcfe9e93a5c9f0810136becfc2c6b074599a8c388437fc1f97820a29ab05b1e42b7a3863d8b411e62447cedb42afb5014
-  languageName: node
-  linkType: hard
-
-"@react-aria/combobox@npm:^3.11.0":
-  version: 3.11.0
-  resolution: "@react-aria/combobox@npm:3.11.0"
-  dependencies:
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/listbox": ^3.13.6
-    "@react-aria/live-announcer": ^3.4.1
-    "@react-aria/menu": ^3.16.0
-    "@react-aria/overlays": ^3.24.0
-    "@react-aria/selection": ^3.21.0
-    "@react-aria/textfield": ^3.15.0
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/combobox": ^3.10.1
-    "@react-stately/form": ^3.1.0
-    "@react-types/button": ^3.10.1
-    "@react-types/combobox": ^3.13.1
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 00c4cc4ef072f2ff453f49dd7fbcd0fffd48960939b4c42bfe688e7c1af13215cf98b13344af252f60d52f9c40c3611c6ab5201402538683b63d2f954c976011
-  languageName: node
-  linkType: hard
-
-"@react-aria/datepicker@npm:^3.12.0":
-  version: 3.12.0
-  resolution: "@react-aria/datepicker@npm:3.12.0"
-  dependencies:
-    "@internationalized/date": ^3.6.0
-    "@internationalized/number": ^3.6.0
-    "@internationalized/string": ^3.2.5
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/form": ^3.0.11
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/label": ^3.7.13
-    "@react-aria/spinbutton": ^3.6.10
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/datepicker": ^3.11.0
-    "@react-stately/form": ^3.1.0
-    "@react-types/button": ^3.10.1
-    "@react-types/calendar": ^3.5.0
-    "@react-types/datepicker": ^3.9.0
-    "@react-types/dialog": ^3.5.14
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 34e203372eb0b9e72402677570cb06ec8126ffb1498f222b193f5830db765fc8a76f84e09b056ef944a4b0d46c0504f5c19a0291ad769d13cd8828dffbd594da
-  languageName: node
-  linkType: hard
-
-"@react-aria/dialog@npm:^3.5.20":
-  version: 3.5.20
-  resolution: "@react-aria/dialog@npm:3.5.20"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/overlays": ^3.24.0
-    "@react-aria/utils": ^3.26.0
-    "@react-types/dialog": ^3.5.14
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 1f1766f528a031a8d2307c9bc6ece41d4bbbf8312193b33779f026863ce43c3193702f03de72b65d88f93c705d7f20895c8eb6a51e3ff39d620add9b89cd1591
-  languageName: node
-  linkType: hard
-
-"@react-aria/disclosure@npm:^3.0.0":
-  version: 3.0.0
-  resolution: "@react-aria/disclosure@npm:3.0.0"
-  dependencies:
-    "@react-aria/ssr": ^3.9.7
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/disclosure": ^3.0.0
-    "@react-types/button": ^3.10.1
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 11fd140d4ef422d636fa31488f3e39bbba5957d89cd218fe8afa8ccb79d35bb2f142fe879c0e18e5e829edc544fce57d5355bf651c6b789235c7d9bd8664220b
-  languageName: node
-  linkType: hard
-
-"@react-aria/dnd@npm:^3.8.0":
-  version: 3.8.0
-  resolution: "@react-aria/dnd@npm:3.8.0"
-  dependencies:
-    "@internationalized/string": ^3.2.5
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/live-announcer": ^3.4.1
-    "@react-aria/overlays": ^3.24.0
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/dnd": ^3.5.0
-    "@react-types/button": ^3.10.1
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 284f98c85f6ad29175c7b9960b5f682abc0d7a349668399f86626f3a81fa55c273e25f0a5a75f210d59c4c192fad20cc1a7d4eecced82f9fa8ac14bcb850e0a9
-  languageName: node
-  linkType: hard
-
-"@react-aria/focus@npm:^3.19.0":
-  version: 3.19.0
-  resolution: "@react-aria/focus@npm:3.19.0"
-  dependencies:
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/utils": ^3.26.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-    clsx: ^2.0.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 5109b24a89ba049cf3b9ffc71ad68fedd8d667a8d9a50a41f334d97db01abf22d144b32ff1ca68f76b7067d9a67e27d5cb13989cd92fcd3734e4e509a04c9ad5
-  languageName: node
-  linkType: hard
-
-"@react-aria/form@npm:^3.0.11":
-  version: 3.0.11
-  resolution: "@react-aria/form@npm:3.0.11"
-  dependencies:
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/form": ^3.1.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 7ea68ad50545178036b003996b8636b8b4dd86921df19703a5609602abb9689cf90689a0038096c8cb46447a1534e25d54d77168e18070832a8d6f42fadecc0b
-  languageName: node
-  linkType: hard
-
-"@react-aria/grid@npm:^3.11.0":
-  version: 3.11.0
-  resolution: "@react-aria/grid@npm:3.11.0"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/live-announcer": ^3.4.1
-    "@react-aria/selection": ^3.21.0
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/grid": ^3.10.0
-    "@react-stately/selection": ^3.18.0
-    "@react-types/checkbox": ^3.9.0
-    "@react-types/grid": ^3.2.10
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 15ae910876e3fee7e667078fc973c51bdced9eb405189ee49dd0eba6d528a88742b407cf32953743ca797cbf66965aaae6467f7da797a96e8f815b3498066e9d
-  languageName: node
-  linkType: hard
-
-"@react-aria/gridlist@npm:^3.10.0":
-  version: 3.10.0
-  resolution: "@react-aria/gridlist@npm:3.10.0"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/grid": ^3.11.0
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/selection": ^3.21.0
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/list": ^3.11.1
-    "@react-stately/tree": ^3.8.6
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 60d82eec9fae0570011b3ce478ac1f66a1b2f9103ed2fe3e788265f78d9dee53cb59ed70f5c6e12446849cb17817a3f754652aebae12890eb6c961b644f3f9c4
-  languageName: node
-  linkType: hard
-
-"@react-aria/i18n@npm:^3.12.4":
-  version: 3.12.4
-  resolution: "@react-aria/i18n@npm:3.12.4"
-  dependencies:
-    "@internationalized/date": ^3.6.0
-    "@internationalized/message": ^3.1.6
-    "@internationalized/number": ^3.6.0
-    "@internationalized/string": ^3.2.5
-    "@react-aria/ssr": ^3.9.7
-    "@react-aria/utils": ^3.26.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: a3d9593bdef208d8b2c23b78e89b9d7a62f62755ef66a77e7b33919fabcbf232f8e4f90ab1d373776487aef461d3b7650b89c33480f1915a1f23184182b062ae
-  languageName: node
-  linkType: hard
-
-"@react-aria/interactions@npm:^3.22.5":
-  version: 3.22.5
-  resolution: "@react-aria/interactions@npm:3.22.5"
-  dependencies:
-    "@react-aria/ssr": ^3.9.7
-    "@react-aria/utils": ^3.26.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: f15c8c343ad6f9725a801a2d931a59a9a0302cd4577e542c3d57ebc1296fcdbd2c75f7cd9f36516ff838f3f3afa2ef2414ba0a514d97663043b7ec07ac8a1611
-  languageName: node
-  linkType: hard
-
-"@react-aria/label@npm:^3.7.13":
-  version: 3.7.13
-  resolution: "@react-aria/label@npm:3.7.13"
-  dependencies:
-    "@react-aria/utils": ^3.26.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 9f3dd8d5a49906ac6aa16c2d27a4afb6b84ef76990855b806fd0bd8335b0727ce2f9cfb9fe83c51f75facc53a707ea565d85d40692d81c646f6017eac1997b2e
-  languageName: node
-  linkType: hard
-
-"@react-aria/link@npm:^3.7.7":
-  version: 3.7.7
-  resolution: "@react-aria/link@npm:3.7.7"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/utils": ^3.26.0
-    "@react-types/link": ^3.5.9
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 9634b30f2104c8a6257cc3ce6dc81818de548649773ee09cd4c2e6fc286de3950a6ab8a9b1f4ee135f014449e0738c41a33f297f339d1e4e0ccddd5763ffc399
-  languageName: node
-  linkType: hard
-
-"@react-aria/listbox@npm:^3.13.6":
-  version: 3.13.6
-  resolution: "@react-aria/listbox@npm:3.13.6"
-  dependencies:
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/label": ^3.7.13
-    "@react-aria/selection": ^3.21.0
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/list": ^3.11.1
-    "@react-types/listbox": ^3.5.3
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: a86e15da3a710d449678f347b3c67a88f9e10713cefc087c57fbe3931d9dc716a6a1e3998b34eaeb7dca0482ba6742c196f43c03ba6d620434d30d3b739d9581
-  languageName: node
-  linkType: hard
-
-"@react-aria/live-announcer@npm:^3.4.1":
-  version: 3.4.1
-  resolution: "@react-aria/live-announcer@npm:3.4.1"
-  dependencies:
-    "@swc/helpers": ^0.5.0
-  checksum: 8f8416c30e359729683e05836b66234cb4156f6166bf6ba023bc0fd4408f2679bac59bd8e6639b629e438b2da292839aa8c293575ad30499f95ea650fccf8a1a
-  languageName: node
-  linkType: hard
-
-"@react-aria/menu@npm:^3.16.0":
-  version: 3.16.0
-  resolution: "@react-aria/menu@npm:3.16.0"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/overlays": ^3.24.0
-    "@react-aria/selection": ^3.21.0
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/menu": ^3.9.0
-    "@react-stately/selection": ^3.18.0
-    "@react-stately/tree": ^3.8.6
-    "@react-types/button": ^3.10.1
-    "@react-types/menu": ^3.9.13
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 39896c9a9f221e69c389ae8cafcdbbfca9235a5a3ab04a0ef3dbc86ae050b0547e8ea72a85baac1731cbe99377240ae9585b2de38844ef81bea39976f87cdeef
-  languageName: node
-  linkType: hard
-
-"@react-aria/meter@npm:^3.4.18":
-  version: 3.4.18
-  resolution: "@react-aria/meter@npm:3.4.18"
-  dependencies:
-    "@react-aria/progress": ^3.4.18
-    "@react-types/meter": ^3.4.5
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: a92c826bf1c88285f4db8aff511ac26eb0381ceba103777ee0bb12681b357d933d07ca3dc45c128d1b266f63a5efe2c534328459201ca299310c9514d8e8f86d
-  languageName: node
-  linkType: hard
-
-"@react-aria/numberfield@npm:^3.11.9":
-  version: 3.11.9
-  resolution: "@react-aria/numberfield@npm:3.11.9"
-  dependencies:
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/spinbutton": ^3.6.10
-    "@react-aria/textfield": ^3.15.0
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/form": ^3.1.0
-    "@react-stately/numberfield": ^3.9.8
-    "@react-types/button": ^3.10.1
-    "@react-types/numberfield": ^3.8.7
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: da8eae831829d13a777a35e0b394fbe9207768702968c5fd62d2b960facea63e70ed6008258b463565562499e0ff47ca4bd425282c6c6ff3574c58a8eb90d51a
-  languageName: node
-  linkType: hard
-
-"@react-aria/overlays@npm:^3.24.0":
-  version: 3.24.0
-  resolution: "@react-aria/overlays@npm:3.24.0"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/ssr": ^3.9.7
-    "@react-aria/utils": ^3.26.0
-    "@react-aria/visually-hidden": ^3.8.18
-    "@react-stately/overlays": ^3.6.12
-    "@react-types/button": ^3.10.1
-    "@react-types/overlays": ^3.8.11
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: dfd176d057ccc9441971706977500ee695786ebaac1719b114402b480335cfb7e058e6281cd911a53b7472300ede259eab345e71deb82fd48defd0f35475ef68
-  languageName: node
-  linkType: hard
-
-"@react-aria/progress@npm:^3.4.18":
-  version: 3.4.18
-  resolution: "@react-aria/progress@npm:3.4.18"
-  dependencies:
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/label": ^3.7.13
-    "@react-aria/utils": ^3.26.0
-    "@react-types/progress": ^3.5.8
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 5445ad6c3db7d9b0f6aec1dbc1cdbd5523cd11285e925702ab79c403b5c161fc8732a2b89508816f879fc44220169c4b5e76dc41b06d1395f20b3c2c54c30932
-  languageName: node
-  linkType: hard
-
-"@react-aria/radio@npm:^3.10.10":
-  version: 3.10.10
-  resolution: "@react-aria/radio@npm:3.10.10"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/form": ^3.0.11
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/label": ^3.7.13
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/radio": ^3.10.9
-    "@react-types/radio": ^3.8.5
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: a85e0ce83485e1458113547e3f1c6a968e05d374bd39d57cf7f2e5bfea1526e771b379c6b036b6843e6e52fd160def94f48206fe90882ff146932f3b05a6be35
-  languageName: node
-  linkType: hard
-
-"@react-aria/searchfield@npm:^3.7.11":
-  version: 3.7.11
-  resolution: "@react-aria/searchfield@npm:3.7.11"
-  dependencies:
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/textfield": ^3.15.0
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/searchfield": ^3.5.8
-    "@react-types/button": ^3.10.1
-    "@react-types/searchfield": ^3.5.10
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 4b0ff36cf1ab44287b62bf4cc41243d90773be95af3aac7e85733d5fd4415fa91a2a86e39d783a4fdd2cff31afa8db2f3021324f68de1f9164ebfac70ba086ac
-  languageName: node
-  linkType: hard
-
-"@react-aria/select@npm:^3.15.0":
-  version: 3.15.0
-  resolution: "@react-aria/select@npm:3.15.0"
-  dependencies:
-    "@react-aria/form": ^3.0.11
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/label": ^3.7.13
-    "@react-aria/listbox": ^3.13.6
-    "@react-aria/menu": ^3.16.0
-    "@react-aria/selection": ^3.21.0
-    "@react-aria/utils": ^3.26.0
-    "@react-aria/visually-hidden": ^3.8.18
-    "@react-stately/select": ^3.6.9
-    "@react-types/button": ^3.10.1
-    "@react-types/select": ^3.9.8
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 9cf3e937995b98a40d8d8896ee38eb39805e67e79419aec451e4a757bb0fbf0df8cebd1699a50c7fa72ae5203e4f581963af6d38becebd20f23b838956b2fe39
-  languageName: node
-  linkType: hard
-
-"@react-aria/selection@npm:^3.21.0":
-  version: 3.21.0
-  resolution: "@react-aria/selection@npm:3.21.0"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/selection": ^3.18.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 612673bbc94b32a47788d057a020585b2416c8f4279760355b80c963efe4587f94aaf2655cbd54f8fbad0197e46fc54612a3291b945a5bd518d899e7bb46e9ae
-  languageName: node
-  linkType: hard
-
-"@react-aria/separator@npm:^3.4.4":
-  version: 3.4.4
-  resolution: "@react-aria/separator@npm:3.4.4"
-  dependencies:
-    "@react-aria/utils": ^3.26.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 9f12848caf1db60c92c7b83901652f38046849001136ac1de39a6259f7d54b6e5606e23f3ae6a8b2f30030313073b841a0b149ddc2aee93a04c6ee9cfbf93b21
-  languageName: node
-  linkType: hard
-
-"@react-aria/slider@npm:^3.7.14":
-  version: 3.7.14
-  resolution: "@react-aria/slider@npm:3.7.14"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/label": ^3.7.13
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/slider": ^3.6.0
-    "@react-types/shared": ^3.26.0
-    "@react-types/slider": ^3.7.7
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 66ec3f7d6f64efaac822bbe47e328708ab006165f2f7cbbea0c10d2507a6afad695c3ed2bdf0369afa7164eeee35b1216b458b27fa51d7ec4e07ddecc672d325
-  languageName: node
-  linkType: hard
-
-"@react-aria/spinbutton@npm:^3.6.10":
-  version: 3.6.10
-  resolution: "@react-aria/spinbutton@npm:3.6.10"
-  dependencies:
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/live-announcer": ^3.4.1
-    "@react-aria/utils": ^3.26.0
-    "@react-types/button": ^3.10.1
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: d6290d05b652e6c2401a724766e5ca4d0d4bfecde17cdcd31e7fe44a4b0037b94b8b02977ce85c9e7b7bc8e0684224cd60428c035fd71a24f0df27173f77cba5
-  languageName: node
-  linkType: hard
-
-"@react-aria/ssr@npm:^3.9.7":
-  version: 3.9.7
-  resolution: "@react-aria/ssr@npm:3.9.7"
-  dependencies:
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 10ad277d8c4db6cf9b546f5800dd084451a4a8173a57b06c6597fd39375526a81f1fb398fe46558d372f8660d33c0a09a2580e0529351d76b2c8938482597b3f
-  languageName: node
-  linkType: hard
-
-"@react-aria/switch@npm:^3.6.10":
-  version: 3.6.10
-  resolution: "@react-aria/switch@npm:3.6.10"
-  dependencies:
-    "@react-aria/toggle": ^3.10.10
-    "@react-stately/toggle": ^3.8.0
-    "@react-types/shared": ^3.26.0
-    "@react-types/switch": ^3.5.7
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: e7c39bdd1316fee95065ce6fed51b7cc496f4147656a489ae4aace9c86b8503c36cd04cd9c135893a1d14d3cb95a30bffd503729075ab7fc1db8add7b1a9f00a
-  languageName: node
-  linkType: hard
-
-"@react-aria/table@npm:^3.16.0":
-  version: 3.16.0
-  resolution: "@react-aria/table@npm:3.16.0"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/grid": ^3.11.0
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/live-announcer": ^3.4.1
-    "@react-aria/utils": ^3.26.0
-    "@react-aria/visually-hidden": ^3.8.18
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/flags": ^3.0.5
-    "@react-stately/table": ^3.13.0
-    "@react-types/checkbox": ^3.9.0
-    "@react-types/grid": ^3.2.10
-    "@react-types/shared": ^3.26.0
-    "@react-types/table": ^3.10.3
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 33f77e1e27dcb5a8bc6c5fe63a737287567d58599e3280406ca26987aac3ee6564479df6070818360644c21902adf3bc87b003997dede6f0afd10b9dfc9945d6
-  languageName: node
-  linkType: hard
-
-"@react-aria/tabs@npm:^3.9.8":
-  version: 3.9.8
-  resolution: "@react-aria/tabs@npm:3.9.8"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/selection": ^3.21.0
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/tabs": ^3.7.0
-    "@react-types/shared": ^3.26.0
-    "@react-types/tabs": ^3.3.11
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 12e19af55286d6299f566b1d31143d18edf0dbfaaf67ebde56b9eecc42668dd01701032bf5fa1bd938da94cacb002a9d8005faf459c5502ceff3aafc4f10ca4a
-  languageName: node
-  linkType: hard
-
-"@react-aria/tag@npm:^3.4.8":
-  version: 3.4.8
-  resolution: "@react-aria/tag@npm:3.4.8"
-  dependencies:
-    "@react-aria/gridlist": ^3.10.0
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/label": ^3.7.13
-    "@react-aria/selection": ^3.21.0
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/list": ^3.11.1
-    "@react-types/button": ^3.10.1
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 78075e07f8697bab32ec5d8f75a55ca0618f361188a9725bae1480e0a45c05d695393ff76c0b52fb65fb38f388a47553a6955a40304ceb9acb37017172586f77
-  languageName: node
-  linkType: hard
-
-"@react-aria/textfield@npm:^3.15.0":
-  version: 3.15.0
-  resolution: "@react-aria/textfield@npm:3.15.0"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/form": ^3.0.11
-    "@react-aria/label": ^3.7.13
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/form": ^3.1.0
-    "@react-stately/utils": ^3.10.5
-    "@react-types/shared": ^3.26.0
-    "@react-types/textfield": ^3.10.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: fb64b469e9e188500f50ae5acdcb1e15236b4c292ab5d0f32beefdc3e26cdce43c9b605ef9a5dcc90cde37909cbac5651e1a0207394d9796c6e59be10200e391
-  languageName: node
-  linkType: hard
-
-"@react-aria/toggle@npm:^3.10.10":
-  version: 3.10.10
-  resolution: "@react-aria/toggle@npm:3.10.10"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/toggle": ^3.8.0
-    "@react-types/checkbox": ^3.9.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 6bb9313691738c0d82d4600a91c232e2c744fb0a3974e97c8593f284a645125eaaafe76e6b4533ad06287077c1eb1dad0776b5e1821f1c7370f204862d256196
-  languageName: node
-  linkType: hard
-
-"@react-aria/toolbar@npm:3.0.0-beta.11":
-  version: 3.0.0-beta.11
-  resolution: "@react-aria/toolbar@npm:3.0.0-beta.11"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/utils": ^3.26.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 8ac5980c2405f011d1594649cf0c8b5fd82e214357250473275ee2e0dff6e66f1a62812f756a7a65210a2716caecfe3a37a416af99e5485dc9a695c2be9ed1dd
-  languageName: node
-  linkType: hard
-
-"@react-aria/tooltip@npm:^3.7.10":
-  version: 3.7.10
-  resolution: "@react-aria/tooltip@npm:3.7.10"
-  dependencies:
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/tooltip": ^3.5.0
-    "@react-types/shared": ^3.26.0
-    "@react-types/tooltip": ^3.4.13
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: a9b00c2d5934a72335cacac99c253555c5cdf8d0e92d06854a622f46d42a24212043d0d0a649869cafd98a757cb2c69f5faffde937ce23cc3aa4dc887948bf3f
-  languageName: node
-  linkType: hard
-
-"@react-aria/tree@npm:3.0.0-beta.2":
-  version: 3.0.0-beta.2
-  resolution: "@react-aria/tree@npm:3.0.0-beta.2"
-  dependencies:
-    "@react-aria/gridlist": ^3.10.0
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/selection": ^3.21.0
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/tree": ^3.8.6
-    "@react-types/button": ^3.10.1
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: a29166a9722688eb21436caaf3fccfaf817aa291ac0333ecb11bdaa6f1e33dd0d83fa8d549a64aee5d6db27992b94668747acb385a191dbcf7cf76c0c8da71c2
-  languageName: node
-  linkType: hard
-
-"@react-aria/utils@npm:^3.26.0":
-  version: 3.26.0
-  resolution: "@react-aria/utils@npm:3.26.0"
-  dependencies:
-    "@react-aria/ssr": ^3.9.7
-    "@react-stately/utils": ^3.10.5
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-    clsx: ^2.0.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 8ad5dbfeaf41e04f6ec2b16e7f0a461614f8d0f94a1b8ce5e19a0f09a79cb49774451db485796e46ef62212ad4978c851fc645351fffbef862a48dcde9b9e1a2
-  languageName: node
-  linkType: hard
-
-"@react-aria/virtualizer@npm:^4.1.0":
-  version: 4.1.0
-  resolution: "@react-aria/virtualizer@npm:4.1.0"
-  dependencies:
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/utils": ^3.26.0
-    "@react-stately/virtualizer": ^4.2.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: c0758442d5de6c054cf2655fba09430603c5c2760a12312b298ff3a6940a6f5d6a1cbc65e2be29cac510662dfcabe560bcf657d768bde314a2d776dc1f8c2a43
-  languageName: node
-  linkType: hard
-
-"@react-aria/visually-hidden@npm:^3.8.18":
-  version: 3.8.18
-  resolution: "@react-aria/visually-hidden@npm:3.8.18"
-  dependencies:
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/utils": ^3.26.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 5169c4d2aea0aebd9135a4fc692f01f1ba58e54de2c9db47a6da4e97e3e6750e14be6beb64718ab1520b878a982523f8056fcf15195247e3ca8624b4e7645d9f
-  languageName: node
-  linkType: hard
-
-"@react-stately/calendar@npm:^3.6.0":
-  version: 3.6.0
-  resolution: "@react-stately/calendar@npm:3.6.0"
-  dependencies:
-    "@internationalized/date": ^3.6.0
-    "@react-stately/utils": ^3.10.5
-    "@react-types/calendar": ^3.5.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 1de64127cef06e3a36ca795981098d389e713cfa21ef9dfcb7c129a50569e0e5008050178629f9d8dcf695eddb8b7a86fcc2e96974764ce2026cbe886d91bf3b
-  languageName: node
-  linkType: hard
-
-"@react-stately/checkbox@npm:^3.6.10":
-  version: 3.6.10
-  resolution: "@react-stately/checkbox@npm:3.6.10"
-  dependencies:
-    "@react-stately/form": ^3.1.0
-    "@react-stately/utils": ^3.10.5
-    "@react-types/checkbox": ^3.9.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 739afb40584a5b73650da1703591511ac36d2b4ef914d024ca0400625bd10500ae977eaabcfb7d8962d8c3ca5740cd4a945e224a145713db1e38292b2b87468b
-  languageName: node
-  linkType: hard
-
-"@react-stately/collections@npm:^3.12.0":
-  version: 3.12.0
-  resolution: "@react-stately/collections@npm:3.12.0"
-  dependencies:
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 7278224dc5b7a757bcba90454afe2b03209b2ae97954782526226664918c75486ab04e418eef69c575d526135dc257125ab1b23db86a40b844dd8766bc5b3eac
-  languageName: node
-  linkType: hard
-
-"@react-stately/color@npm:^3.8.1":
-  version: 3.8.1
-  resolution: "@react-stately/color@npm:3.8.1"
-  dependencies:
-    "@internationalized/number": ^3.6.0
-    "@internationalized/string": ^3.2.5
-    "@react-aria/i18n": ^3.12.4
-    "@react-stately/form": ^3.1.0
-    "@react-stately/numberfield": ^3.9.8
-    "@react-stately/slider": ^3.6.0
-    "@react-stately/utils": ^3.10.5
-    "@react-types/color": ^3.0.1
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 9e1aa47f3131c42c17e1f1f708215bfdf10548ffe884e0fd5058ab795b19d437a94578f2eeaeb23d194d6373d947a34284c00b13e95f9023fa16d40c3e7f2da9
-  languageName: node
-  linkType: hard
-
-"@react-stately/combobox@npm:^3.10.1":
-  version: 3.10.1
-  resolution: "@react-stately/combobox@npm:3.10.1"
-  dependencies:
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/form": ^3.1.0
-    "@react-stately/list": ^3.11.1
-    "@react-stately/overlays": ^3.6.12
-    "@react-stately/select": ^3.6.9
-    "@react-stately/utils": ^3.10.5
-    "@react-types/combobox": ^3.13.1
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 90434b4b54572fcf487d53dc33d067e4379884753522ca1d1374c26db017cbd9f2bee0c984bd4c0741b7f77fbdb487e120490997e4bc502288ed1121e0a6e32f
-  languageName: node
-  linkType: hard
-
-"@react-stately/data@npm:^3.12.0":
-  version: 3.12.0
-  resolution: "@react-stately/data@npm:3.12.0"
-  dependencies:
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: f4e99854f612d1fc1c590a44b938e66dd3bf75303fda0b1e66368ab83ff9e169907a6f71c768d579f69b81a585671a72bef1c2dc6848afe7a439f54b5998643e
-  languageName: node
-  linkType: hard
-
-"@react-stately/datepicker@npm:^3.11.0":
-  version: 3.11.0
-  resolution: "@react-stately/datepicker@npm:3.11.0"
-  dependencies:
-    "@internationalized/date": ^3.6.0
-    "@internationalized/string": ^3.2.5
-    "@react-stately/form": ^3.1.0
-    "@react-stately/overlays": ^3.6.12
-    "@react-stately/utils": ^3.10.5
-    "@react-types/datepicker": ^3.9.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: a1b0126682454ff2ca7c10a18b5a70c783b18e39b5cf82c63484789c8f64ca9e633934be856d79b5faa97867beb3f34e7085b58bcacfac1289c598aca6e4a2a8
-  languageName: node
-  linkType: hard
-
-"@react-stately/disclosure@npm:^3.0.0":
-  version: 3.0.0
-  resolution: "@react-stately/disclosure@npm:3.0.0"
-  dependencies:
-    "@react-stately/utils": ^3.10.5
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: f70a2cceb097c2c97634d66ef682acb1797d2db1b0d3f31a92e2cae55786194727ef921cb6a72ea054ed8400c6de6ba32887aa3d2436ccac3139d95f549f0b61
-  languageName: node
-  linkType: hard
-
-"@react-stately/dnd@npm:^3.5.0":
-  version: 3.5.0
-  resolution: "@react-stately/dnd@npm:3.5.0"
-  dependencies:
-    "@react-stately/selection": ^3.18.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 6fab7824b07b440099fd85affed0db39487db61b1ac7cb52f7e65c5af32d0b79edb1fbca39a41475e1755361d3b0f47443275b5cf79cd8692abfd3fb1c7e09f4
-  languageName: node
-  linkType: hard
-
-"@react-stately/flags@npm:^3.0.5":
-  version: 3.0.5
-  resolution: "@react-stately/flags@npm:3.0.5"
-  dependencies:
-    "@swc/helpers": ^0.5.0
-  checksum: 8a2aaacd77bac14ea8e71726350bc30bd252fe5bcd70a72a26da5d433014788e1395ef0c3cb878492de9758e44243fb6470585e697874109c3924e1699a94fc7
-  languageName: node
-  linkType: hard
-
-"@react-stately/form@npm:^3.1.0":
-  version: 3.1.0
-  resolution: "@react-stately/form@npm:3.1.0"
-  dependencies:
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: e83eeaee262e770c751a898f12ff5c467954fee687edc9cafa65cfc9b6e1739d5397e0902f134f6a94bb3716295f19e6c98b0048cf7167b78bdb9f77db2ff89a
-  languageName: node
-  linkType: hard
-
-"@react-stately/grid@npm:^3.10.0":
-  version: 3.10.0
-  resolution: "@react-stately/grid@npm:3.10.0"
-  dependencies:
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/selection": ^3.18.0
-    "@react-types/grid": ^3.2.10
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: a90c00019f7264da522c7b82ef9ec637287034976eae091314714fbc32a088054ed3b154c62f467fc3538beadf135285cd6a98b2fe4dd6e29dfbf67938189e87
-  languageName: node
-  linkType: hard
-
-"@react-stately/layout@npm:^4.1.0":
-  version: 4.1.0
-  resolution: "@react-stately/layout@npm:4.1.0"
-  dependencies:
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/table": ^3.13.0
-    "@react-stately/virtualizer": ^4.2.0
-    "@react-types/grid": ^3.2.10
-    "@react-types/shared": ^3.26.0
-    "@react-types/table": ^3.10.3
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 2ca1df97728833885f09cc3f44fb24d164108c0062b38ad6f5c1d3157720e0d02ff18205d05233b4bf19f63b9059353e7489f00517aefb9b0f6d8168704f013b
-  languageName: node
-  linkType: hard
-
-"@react-stately/list@npm:^3.11.1":
-  version: 3.11.1
-  resolution: "@react-stately/list@npm:3.11.1"
-  dependencies:
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/selection": ^3.18.0
-    "@react-stately/utils": ^3.10.5
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 302fed3798d76da96d6d2a198bee126c8b5967542fd4722fa1b6f740cc33f6df5e1349bdf61353b3e546805107f80eaddd79d39e30611fd150b298c974879abe
-  languageName: node
-  linkType: hard
-
-"@react-stately/menu@npm:^3.9.0":
-  version: 3.9.0
-  resolution: "@react-stately/menu@npm:3.9.0"
-  dependencies:
-    "@react-stately/overlays": ^3.6.12
-    "@react-types/menu": ^3.9.13
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: b66c76308c609f3b8b5494c8b1fa6e695a05c147d9dcb927af37fcfb70b231ca0f69cb1b42dd8fbdb83df52d9ddda4763bfe784d86693e7c5f9f25fa131a06b1
-  languageName: node
-  linkType: hard
-
-"@react-stately/numberfield@npm:^3.9.8":
-  version: 3.9.8
-  resolution: "@react-stately/numberfield@npm:3.9.8"
-  dependencies:
-    "@internationalized/number": ^3.6.0
-    "@react-stately/form": ^3.1.0
-    "@react-stately/utils": ^3.10.5
-    "@react-types/numberfield": ^3.8.7
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 67187c1b5c7323feeb17966d468141bfba6380e91bf992d10470c1c6aeae0961be0b357e28bb46da23d81619b2eea88284a27fe06d67e6ed975841f7e7a3f153
-  languageName: node
-  linkType: hard
-
-"@react-stately/overlays@npm:^3.6.12":
-  version: 3.6.12
-  resolution: "@react-stately/overlays@npm:3.6.12"
-  dependencies:
-    "@react-stately/utils": ^3.10.5
-    "@react-types/overlays": ^3.8.11
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 6be299650614f2a9d3103540eb76c8b049757ebc27c358c86a32ad6e35aff01c19f708877cfa3549b1b1173531d067359336dcfbf3d38ea81f7d63f8ca9dd9a1
-  languageName: node
-  linkType: hard
-
-"@react-stately/radio@npm:^3.10.9":
-  version: 3.10.9
-  resolution: "@react-stately/radio@npm:3.10.9"
-  dependencies:
-    "@react-stately/form": ^3.1.0
-    "@react-stately/utils": ^3.10.5
-    "@react-types/radio": ^3.8.5
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: b15046c5f38f0ad9cf3bbdc169733dfb901a5532e8dcf2ff71abf112a2378767e5f5b3c628f1d261b2db8f15f771afe972c12d76d0437ed19101c995bc909ab9
-  languageName: node
-  linkType: hard
-
-"@react-stately/searchfield@npm:^3.5.8":
-  version: 3.5.8
-  resolution: "@react-stately/searchfield@npm:3.5.8"
-  dependencies:
-    "@react-stately/utils": ^3.10.5
-    "@react-types/searchfield": ^3.5.10
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: cd7d427c744490f6d77387564ebb718091ff0ffcbf164472c92f1f79d5cfafe7def03a0ea9a34175e37fe440184d2992bc9336423a68f846577095063f33702d
-  languageName: node
-  linkType: hard
-
-"@react-stately/select@npm:^3.6.9":
-  version: 3.6.9
-  resolution: "@react-stately/select@npm:3.6.9"
-  dependencies:
-    "@react-stately/form": ^3.1.0
-    "@react-stately/list": ^3.11.1
-    "@react-stately/overlays": ^3.6.12
-    "@react-types/select": ^3.9.8
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 41e9ed9df52eaa542d54e81561e23c7899f9a6998a2365dc77d2685e6672a5c8c32ca9331680cacd6b9a6e0092ad8235581d1324b240acc78da7b60512b612c5
-  languageName: node
-  linkType: hard
-
-"@react-stately/selection@npm:^3.18.0":
-  version: 3.18.0
-  resolution: "@react-stately/selection@npm:3.18.0"
-  dependencies:
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/utils": ^3.10.5
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 0ae179f7b082bcc472e3ddfa585ed46d5304d1ac21c720d14c394f3030772c510318122fc095801a66b005c2174cfc7ea37298fb929a26993d73194a8bde0324
-  languageName: node
-  linkType: hard
-
-"@react-stately/slider@npm:^3.6.0":
-  version: 3.6.0
-  resolution: "@react-stately/slider@npm:3.6.0"
-  dependencies:
-    "@react-stately/utils": ^3.10.5
-    "@react-types/shared": ^3.26.0
-    "@react-types/slider": ^3.7.7
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 9c469aeec52d41b03d72a56e024294d9173bec630a5014e3c00fb968b9aba443e2fa1fdfb8b18c972452d018b1478eafe74f391cc5a61e49ac0e121bc6264348
-  languageName: node
-  linkType: hard
-
-"@react-stately/table@npm:^3.13.0":
-  version: 3.13.0
-  resolution: "@react-stately/table@npm:3.13.0"
-  dependencies:
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/flags": ^3.0.5
-    "@react-stately/grid": ^3.10.0
-    "@react-stately/selection": ^3.18.0
-    "@react-stately/utils": ^3.10.5
-    "@react-types/grid": ^3.2.10
-    "@react-types/shared": ^3.26.0
-    "@react-types/table": ^3.10.3
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: be03b35ad62ef3e8c95c412c9a9e06a3c7099dc0ab2d1a4e97316f7bce51e414b9e135724b1bac4104b33a1f432fefc958b348901b86e93b237d4e793205d44d
-  languageName: node
-  linkType: hard
-
-"@react-stately/tabs@npm:^3.7.0":
-  version: 3.7.0
-  resolution: "@react-stately/tabs@npm:3.7.0"
-  dependencies:
-    "@react-stately/list": ^3.11.1
-    "@react-types/shared": ^3.26.0
-    "@react-types/tabs": ^3.3.11
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 831bf6f12b055867104eeb3a0d56c0a117202eda4161f585e14a376c74656ba0d5e87b90aac2196e68ce71e9f23ed590e4284e021f5fa63ae32959a3c6af4ddf
-  languageName: node
-  linkType: hard
-
-"@react-stately/toggle@npm:^3.8.0":
-  version: 3.8.0
-  resolution: "@react-stately/toggle@npm:3.8.0"
-  dependencies:
-    "@react-stately/utils": ^3.10.5
-    "@react-types/checkbox": ^3.9.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 78732515225f2ce7cad352324af808bd7e1437a184f39e1afa54a00d83695e54676e876d61b4bf6c2f43ddf009e819615b9892827a1b455238c216210e4b5377
-  languageName: node
-  linkType: hard
-
-"@react-stately/tooltip@npm:^3.5.0":
-  version: 3.5.0
-  resolution: "@react-stately/tooltip@npm:3.5.0"
-  dependencies:
-    "@react-stately/overlays": ^3.6.12
-    "@react-types/tooltip": ^3.4.13
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 3b103fa2c8b0413c50e477225572b5f18d8235b0ddde3913d0e6afe448b126a7580690591d35893f5da07f65ac61c90fc040b755f3f4eda8e4c6e0017800db04
-  languageName: node
-  linkType: hard
-
-"@react-stately/tree@npm:^3.8.6":
-  version: 3.8.6
-  resolution: "@react-stately/tree@npm:3.8.6"
-  dependencies:
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/selection": ^3.18.0
-    "@react-stately/utils": ^3.10.5
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: df540b7fea2b1f41201ceed927ba4d37fa0f1e758c75f4a1d2e5f2b8eceabaa7e14513523a73101f8013bc8f8792e75f100157d50830928655556affe841e41c
-  languageName: node
-  linkType: hard
-
-"@react-stately/utils@npm:^3.10.5":
-  version: 3.10.5
-  resolution: "@react-stately/utils@npm:3.10.5"
-  dependencies:
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 4f4292ccf7bb86578a20b354cf9569f88d2d50ecb2e10ac6046fab3b9eb2175f734acf1b9bd87787e439220b912785a54551a724ab285f03e4f33b2942831f57
-  languageName: node
-  linkType: hard
-
-"@react-stately/virtualizer@npm:^4.2.0":
-  version: 4.2.0
-  resolution: "@react-stately/virtualizer@npm:4.2.0"
-  dependencies:
-    "@react-aria/utils": ^3.26.0
-    "@react-types/shared": ^3.26.0
-    "@swc/helpers": ^0.5.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 862c5edacadd927f248ce1e7b28c7082508e3f699da5ef2aabedf1d94a86c1e8aee764ad43368d94e3de0dee3f6da0415a77da4a9a45d22008952d7618a8d5de
-  languageName: node
-  linkType: hard
-
-"@react-types/breadcrumbs@npm:^3.7.9":
-  version: 3.7.9
-  resolution: "@react-types/breadcrumbs@npm:3.7.9"
-  dependencies:
-    "@react-types/link": ^3.5.9
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 707114b57d986daba02808d04d0c38570cfacd2e4e44dcc923c1fd72807797cce4af4c7278f0d6afff68b316ec5f8576959ac50f50b3e6787bd6ad14bbaa3854
-  languageName: node
-  linkType: hard
-
-"@react-types/button@npm:^3.10.1":
-  version: 3.10.1
-  resolution: "@react-types/button@npm:3.10.1"
-  dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 35d783d8f5eddebec3947791d7e166c41f80cd052956da3899f36e6ce112b13af549c9521c321995a27add57a759934b0a8ad7c6a3038be221454a0e4019d0db
-  languageName: node
-  linkType: hard
-
-"@react-types/calendar@npm:^3.5.0":
-  version: 3.5.0
-  resolution: "@react-types/calendar@npm:3.5.0"
-  dependencies:
-    "@internationalized/date": ^3.6.0
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 467ee28ec5dfc3cac8f1899059208394c77cb41d2e0aad92db723a57de9f104c9917e9f83c368b55f8d2a6a47521d259f8c9d939d0920478336cbf2ef157d35d
-  languageName: node
-  linkType: hard
-
-"@react-types/checkbox@npm:^3.9.0":
-  version: 3.9.0
-  resolution: "@react-types/checkbox@npm:3.9.0"
-  dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 5da37fe94e9e6e544b00313c7eaacd1b349a5da69c6a89dad1d822161ba29d4304c0201d12dda141f557caec5b1f297e3c283d49e5f880c8b274ef4f6cc01f09
-  languageName: node
-  linkType: hard
-
-"@react-types/color@npm:^3.0.1":
-  version: 3.0.1
-  resolution: "@react-types/color@npm:3.0.1"
-  dependencies:
-    "@react-types/shared": ^3.26.0
-    "@react-types/slider": ^3.7.7
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 5b4f4e606d44b9c2fec9f72dfac9e124fd7b491604646d47583ed3e5323803204e56c29085aa51d2da1fe152a61d522958fdc5238d5c7def5a5db2ec55de35e7
-  languageName: node
-  linkType: hard
-
-"@react-types/combobox@npm:^3.13.1":
-  version: 3.13.1
-  resolution: "@react-types/combobox@npm:3.13.1"
-  dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 726522ca43131dfcac45f410c47834715b2668dd6fa80cde5e46835cceed5f82fc6157f23a53b3a5cafcf2e6e7d860bd61194dcf393e02f72e7bed358317332b
-  languageName: node
-  linkType: hard
-
-"@react-types/datepicker@npm:^3.9.0":
-  version: 3.9.0
-  resolution: "@react-types/datepicker@npm:3.9.0"
-  dependencies:
-    "@internationalized/date": ^3.6.0
-    "@react-types/calendar": ^3.5.0
-    "@react-types/overlays": ^3.8.11
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 5a7d734babd1e07835b50fb077e798137c235082633cd3aa93ff700c0836eb801df39bf6bc046143db2c50ebb747829eda65bb81b22d70cde2761bfefc87e19f
+  conditions: os=linux & cpu=x64 & libc=glibc
   languageName: node
   linkType: hard
 
-"@react-types/dialog@npm:^3.5.14":
-  version: 3.5.14
-  resolution: "@react-types/dialog@npm:3.5.14"
-  dependencies:
-    "@react-types/overlays": ^3.8.11
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 99f7c4789feef7a99a7e85ae861a8299e84133848824933fe2534bf045b932af8da6bd6d65f113a5d993910a1f3dc5e71ab9c6a204287e3bc58c781d18fe408b
+"@next/swc-linux-x64-musl@npm:12.3.4":
+  version: 12.3.4
+  resolution: "@next/swc-linux-x64-musl@npm:12.3.4"
+  conditions: os=linux & cpu=x64 & libc=musl
   languageName: node
   linkType: hard
 
-"@react-types/form@npm:^3.7.8":
-  version: 3.7.8
-  resolution: "@react-types/form@npm:3.7.8"
-  dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: eb6771ab4628d242c1f5c3a27fe7f29628368c28e646bb67ad950f022246a8ecc35c6d3f49a71b1cc0fba1fb7ce1c5b8aaa0e3331763b51eaea6cbbfcb3a6ce5
+"@next/swc-win32-arm64-msvc@npm:12.3.4":
+  version: 12.3.4
+  resolution: "@next/swc-win32-arm64-msvc@npm:12.3.4"
+  conditions: os=win32 & cpu=arm64
   languageName: node
   linkType: hard
 
-"@react-types/grid@npm:^3.2.10":
-  version: 3.2.10
-  resolution: "@react-types/grid@npm:3.2.10"
-  dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 49f3a933ce9e62e78a309eb9f0ef80d29583a5e96b4f9b455f3c04fb40839f758d2b7a87b22bf9c846c3d0a71d39a9201951aa3e5ae0107330aa63ee5af29514
+"@next/swc-win32-ia32-msvc@npm:12.3.4":
+  version: 12.3.4
+  resolution: "@next/swc-win32-ia32-msvc@npm:12.3.4"
+  conditions: os=win32 & cpu=ia32
   languageName: node
   linkType: hard
 
-"@react-types/link@npm:^3.5.9":
-  version: 3.5.9
-  resolution: "@react-types/link@npm:3.5.9"
-  dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 8d04d420fe287c71ae3130b92e9457f35dbfeb06228f57e606e9c8f9d3431e7920e04c81cfcce887ae51d255a524ba442be902c20e88ab1cbbf9703afd6f0fa7
+"@next/swc-win32-x64-msvc@npm:12.3.4":
+  version: 12.3.4
+  resolution: "@next/swc-win32-x64-msvc@npm:12.3.4"
+  conditions: os=win32 & cpu=x64
   languageName: node
   linkType: hard
 
-"@react-types/listbox@npm:^3.5.3":
-  version: 3.5.3
-  resolution: "@react-types/listbox@npm:3.5.3"
-  dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 00170013a05a794d3bff1df6ed65f09f1e4b9d0517a2a28b7724eb847cd1d3d5f1a756ddf0831143b8bbef57d9f07013e8c2235f2010176575f6c5cbf5d5f7ce
+"@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3":
+  version: 2.1.8-no-fsevents.3
+  resolution: "@nicolo-ribaudo/chokidar-2@npm:2.1.8-no-fsevents.3"
+  checksum: ee55cc9241aeea7eb94b8a8551bfa4246c56c53bc71ecda0a2104018fcc328ba5723b33686bdf9cc65d4df4ae65e8016b89e0bbdeb94e0309fe91bb9ced42344
   languageName: node
   linkType: hard
 
-"@react-types/menu@npm:^3.9.13":
-  version: 3.9.13
-  resolution: "@react-types/menu@npm:3.9.13"
-  dependencies:
-    "@react-types/overlays": ^3.8.11
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 6e96dc7faa10d731385015185d50cc79f3595eee2ab713261121753ba59477635a504559097fac781af139ea45935e2a81e9dedb1b70ae2878a4f49f63704bea
+"@node-kit/extra.fs@npm:3.2.0":
+  version: 3.2.0
+  resolution: "@node-kit/extra.fs@npm:3.2.0"
+  checksum: 48781d37ddd45f544774c17fccf31e1bfe648a16354cf8b20b28f0315798d977336a50c2a4cbb421fd9016792a0860cb2254e7450885324e7ace08903176b58b
   languageName: node
   linkType: hard
 
-"@react-types/meter@npm:^3.4.5":
-  version: 3.4.5
-  resolution: "@react-types/meter@npm:3.4.5"
+"@node-kit/yarn-workspace-root@npm:^3.2.0":
+  version: 3.2.0
+  resolution: "@node-kit/yarn-workspace-root@npm:3.2.0"
   dependencies:
-    "@react-types/progress": ^3.5.8
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 415a27294b9098b0614ef95893781e9564d4046a490fcb532b12ab521d3f9c2b76be3d4d2fba15773feb88083cb8e4d57942dbf3c05c5e393b4dceccbefc6cd3
+    "@node-kit/extra.fs": 3.2.0
+    find-up: ^5.0.0
+    micromatch: ^4.0.5
+  checksum: 18eca9649017f1b419a230909c319d57fe26400d3074685bb89946be30b3eb6670594dc7bb20d1a4d83cb4b991acf9818026b214fb879717f5ca0290ed934c3e
   languageName: node
   linkType: hard
 
-"@react-types/numberfield@npm:^3.8.7":
-  version: 3.8.7
-  resolution: "@react-types/numberfield@npm:3.8.7"
+"@nodelib/fs.scandir@npm:2.1.5":
+  version: 2.1.5
+  resolution: "@nodelib/fs.scandir@npm:2.1.5"
   dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 72ad06684d9e1c1f4d1c5ec8b108ff4e9fba9c36cae8737efc57021e1e114255ba9094019ac38e20681addf6d06bef25c3f3b9af2470317b963ab10b1d60cd9b
+    "@nodelib/fs.stat": 2.0.5
+    run-parallel: ^1.1.9
+  checksum: a970d595bd23c66c880e0ef1817791432dbb7acbb8d44b7e7d0e7a22f4521260d4a83f7f9fd61d44fda4610105577f8f58a60718105fb38352baed612fd79e59
   languageName: node
   linkType: hard
 
-"@react-types/overlays@npm:^3.8.11":
-  version: 3.8.11
-  resolution: "@react-types/overlays@npm:3.8.11"
-  dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: adde53027f40991874519edb14edf763ba61310f837850985d934c3493dc646f2c0d5b0eb507e00d1c89631105b742a9a27a73d9e7a0fb9a3eb6d82a5692dbf5
+"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2":
+  version: 2.0.5
+  resolution: "@nodelib/fs.stat@npm:2.0.5"
+  checksum: 012480b5ca9d97bff9261571dbbec7bbc6033f69cc92908bc1ecfad0792361a5a1994bc48674b9ef76419d056a03efadfce5a6cf6dbc0a36559571a7a483f6f0
   languageName: node
   linkType: hard
 
-"@react-types/progress@npm:^3.5.8":
-  version: 3.5.8
-  resolution: "@react-types/progress@npm:3.5.8"
+"@nodelib/fs.walk@npm:^1.2.3":
+  version: 1.2.8
+  resolution: "@nodelib/fs.walk@npm:1.2.8"
   dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: c2333df01c47c89359f545c1723307f744b1eab7c618dad220eae1a8d80645fb2330c63780444fd06bda3e0ec807b094be7c018141391bec6fa6f62986e92bcf
+    "@nodelib/fs.scandir": 2.1.5
+    fastq: ^1.6.0
+  checksum: 190c643f156d8f8f277bf2a6078af1ffde1fd43f498f187c2db24d35b4b4b5785c02c7dc52e356497b9a1b65b13edc996de08de0b961c32844364da02986dc53
   languageName: node
   linkType: hard
 
-"@react-types/radio@npm:^3.8.5":
-  version: 3.8.5
-  resolution: "@react-types/radio@npm:3.8.5"
+"@npmcli/fs@npm:^2.1.0":
+  version: 2.1.0
+  resolution: "@npmcli/fs@npm:2.1.0"
   dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 9ba139ae4d6814a6bd3849b446324d35970a16937495ec4354e1b9b0fcfbf26d590db6f010613c7af609710b6b828c229c65299bbdf78542effffea8ad127b67
+    "@gar/promisify": ^1.1.3
+    semver: ^7.3.5
+  checksum: 6ec6d678af6da49f9dac50cd882d7f661934dd278972ffbaacde40d9eaa2871292d634000a0cca9510f6fc29855fbd4af433e1adbff90a524ec3eaf140f1219b
   languageName: node
   linkType: hard
 
-"@react-types/searchfield@npm:^3.5.10":
-  version: 3.5.10
-  resolution: "@react-types/searchfield@npm:3.5.10"
+"@npmcli/move-file@npm:^2.0.0":
+  version: 2.0.0
+  resolution: "@npmcli/move-file@npm:2.0.0"
   dependencies:
-    "@react-types/shared": ^3.26.0
-    "@react-types/textfield": ^3.10.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 4b6181ed064bfbdc90b336900f684f2216b58ebe14579fee5ad8d4a8116fff58ed555dd9929ed71a08cbdcb80e2e61be68183cad930761a28a44dd5fe195ee90
+    mkdirp: ^1.0.4
+    rimraf: ^3.0.2
+  checksum: 1388777b507b0c592d53f41b9d182e1a8de7763bc625fc07999b8edbc22325f074e5b3ec90af79c89d6987fdb2325bc66d59f483258543c14a43661621f841b0
   languageName: node
   linkType: hard
 
-"@react-types/select@npm:^3.9.8":
-  version: 3.9.8
-  resolution: "@react-types/select@npm:3.9.8"
+"@npmcli/promise-spawn@npm:^7.0.0":
+  version: 7.0.0
+  resolution: "@npmcli/promise-spawn@npm:7.0.0"
   dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 4ae8366a72d84ef979c5be3c283bc417b791dcb2c5c52bfb670c2c352f6be50bbe7e1159349e3150e610f6ef21a4b463a8f33c958e8506120e13d957d4851973
+    which: ^4.0.0
+  checksum: 22a8c4fd4ef2729cf75d13b0b294e8c695e08bdb2143e951288056656091fc5281e8baf330c97a6bc803e6fc09489028bf80dcd787972597ef9fda9a9349fc0f
   languageName: node
   linkType: hard
 
-"@react-types/shared@npm:^3.26.0":
-  version: 3.26.0
-  resolution: "@react-types/shared@npm:3.26.0"
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: f51381af98a89e1a9823ee18ed16418c5e8badd640dffb0a3523437aa003b144eea878bb49b4f62672484c361f380864d8dcaba742259da32a67b29692a63b06
+"@pkgjs/parseargs@npm:^0.11.0":
+  version: 0.11.0
+  resolution: "@pkgjs/parseargs@npm:0.11.0"
+  checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f
   languageName: node
   linkType: hard
 
-"@react-types/slider@npm:^3.7.7":
-  version: 3.7.7
-  resolution: "@react-types/slider@npm:3.7.7"
+"@playground/next-playground@workspace:playgrounds/next-playground":
+  version: 0.0.0-use.local
+  resolution: "@playground/next-playground@workspace:playgrounds/next-playground"
   dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 8ddd93140178d1166d35059b3a3780c4e79aafc1050f957171ddbf0cb9a9b29a0f37fe14706fa36776a1a762619927b777870972e017592ab21844e52e32aa33
-  languageName: node
-  linkType: hard
+    "@builder.io/partytown": ^0.7.4
+    "@next/bundle-analyzer": ^12.1.5
+    "@segment/analytics-next": "workspace:^"
+    "@types/faker": ^5.1.2
+    "@types/react": ^17.0.37
+    eslint-config-next: ^12.1.6
+    faker: ^5.1.0
+    lodash: ^4.17.21
+    next: ^12.1.0
+    prismjs: ^1.27.0
+    rc-table: ^7.10.0
+    react: ^17.0.2
+    react-dom: ^17.0.2
+    react-json-tree: ^0.13.0
+    react-simple-code-editor: ^0.11.0
+    source-map-loader: ^3.0.1
+  languageName: unknown
+  linkType: soft
 
-"@react-types/switch@npm:^3.5.7":
-  version: 3.5.7
-  resolution: "@react-types/switch@npm:3.5.7"
+"@playground/standalone-playground@workspace:playgrounds/standalone-playground":
+  version: 0.0.0-use.local
+  resolution: "@playground/standalone-playground@workspace:playgrounds/standalone-playground"
   dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 9bb6dfe7a6e9eae5b7979a0db23dd0daca5724469af24132c54479bcc71b2e21bc2826e22001b2f7f842077e00537d07f884844b6e1b1a570f9c2aeb393d4b76
-  languageName: node
-  linkType: hard
+    "@segment/analytics-consent-wrapper-onetrust": "workspace:^"
+    "@segment/analytics-next": "workspace:^"
+    http-server: 14.1.1
+  languageName: unknown
+  linkType: soft
 
-"@react-types/table@npm:^3.10.3":
-  version: 3.10.3
-  resolution: "@react-types/table@npm:3.10.3"
+"@playground/with-vite@workspace:playgrounds/with-vite":
+  version: 0.0.0-use.local
+  resolution: "@playground/with-vite@workspace:playgrounds/with-vite"
   dependencies:
-    "@react-types/grid": ^3.2.10
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 34e6721e8357b304f2ad333003abf99bd74c55100a8d17f98c4e217a86adb3174f9fc724707472776cb2e1134a70a5a8305ebeb7a47286e81b95241d391f556c
-  languageName: node
-  linkType: hard
+    "@segment/analytics-next": "workspace:^"
+    "@types/react": ^18
+    "@types/react-dom": ^18
+    "@vitejs/plugin-react": ^1.3.0
+    react: ^18.0.0
+    react-dom: ^18.0.0
+    typescript: ^4.7.0
+    vite: ^2.9.18
+  languageName: unknown
+  linkType: soft
 
-"@react-types/tabs@npm:^3.3.11":
-  version: 3.3.11
-  resolution: "@react-types/tabs@npm:3.3.11"
+"@playwright/test@npm:^1.28.1":
+  version: 1.28.1
+  resolution: "@playwright/test@npm:1.28.1"
   dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 6b8bea0de3fcea7061079bc6042fdf3ebb815c39f5b0d53084a460801a47797ebc686c244f89242f78992abde9afa20604cbc8485a89b94cb35a81f64659aa35
+    "@types/node": "*"
+    playwright-core: 1.28.1
+  bin:
+    playwright: cli.js
+  checksum: dc39dfdf848171a6c65fc32a9dbc95162684a4a1e3401dd157d7d6822a065d8dcb96b2484fc3b223baea4da774450fddaeaa6d4d21546d17d45f01884fa8d7c5
   languageName: node
   linkType: hard
 
-"@react-types/textfield@npm:^3.10.0":
-  version: 3.10.0
-  resolution: "@react-types/textfield@npm:3.10.0"
-  dependencies:
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 34899054212a44f615ca2e5ca13176dbe06a55528f07794a25adb1383d40be0cd53a28b10047b88526df46561eaabeb89ded7719fef1fc5b4aa9381eceb6eef6
+"@polka/url@npm:^1.0.0-next.9":
+  version: 1.0.0-next.11
+  resolution: "@polka/url@npm:1.0.0-next.11"
+  checksum: db1626fb6d7167ce2de6223c95f0a5ff8e1e7c56b2e8709f904f219d8fcc7b075de842ea8bf0ed7af9f5bc350b166b286b241636982f10d0f02964f34215a0e0
   languageName: node
   linkType: hard
 
-"@react-types/tooltip@npm:^3.4.13":
-  version: 3.4.13
-  resolution: "@react-types/tooltip@npm:3.4.13"
+"@rc-component/context@npm:^1.4.0":
+  version: 1.4.0
+  resolution: "@rc-component/context@npm:1.4.0"
   dependencies:
-    "@react-types/overlays": ^3.8.11
-    "@react-types/shared": ^3.26.0
+    "@babel/runtime": ^7.10.1
+    rc-util: ^5.27.0
   peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: fb63e7ecac075a87416a69f53ac0391b26b884a17148f8cca042a4700f9994b49e0a3b3231bd47598e43c2cd38d032b6edcc5a7b8853f04887409a2e1f474ecd
-  languageName: node
-  linkType: hard
-
-"@remix-run/router@npm:1.16.1":
-  version: 1.16.1
-  resolution: "@remix-run/router@npm:1.16.1"
-  checksum: 69068815832b30d2a5c063ac1c75365c45cf5b484dab65e1b3129fdbb3c2a7b866401733f766e550dbca1eaf0b84bc772a9c55310f4dd21eb53e62eb1b4625d0
+    react: ">=16.9.0"
+    react-dom: ">=16.9.0"
+  checksum: 3771237de1e82a453cfff7b5f0ca0dcc370a2838be8ecbfe172c26dec2e94dc2354a8b3061deaff7e633e418fc1b70ce3d10d770603f12dc477fe03f2ada7059
   languageName: node
   linkType: hard
 
@@ -6184,35 +4336,6 @@ __metadata:
   languageName: unknown
   linkType: soft
 
-"@segment/analytics-signals-runtime@npm:2.0.0":
-  version: 2.0.0
-  resolution: "@segment/analytics-signals-runtime@npm:2.0.0"
-  dependencies:
-    tslib: ^2.4.1
-  checksum: 4df82a041bb66514de606b29866f59cf3ecac449766a649d9d4cb586e52183d5f3b866c3cb041774e259331c9fa38181325f1a5ab4528202cf86c3e9be03906f
-  languageName: node
-  linkType: hard
-
-"@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"
-  dependencies:
-    "@internal/config-webpack": "workspace:^"
-    "@internal/test-helpers": "workspace:^"
-    "@segment/analytics-generic-utils": 1.2.0
-    "@segment/analytics-signals-runtime": 2.0.0
-    fake-indexeddb: ^6.0.0
-    idb: ^8.0.0
-    node-fetch: ^2.6.7
-    tslib: ^2.4.1
-  peerDependencies:
-    "@segment/analytics-next": ">=1.78.0"
-  peerDependenciesMeta:
-    "@segment/analytics-next":
-      optional: true
-  languageName: unknown
-  linkType: soft
-
 "@segment/analytics.js-integration-amplitude@npm:^3.3.3":
   version: 3.3.3
   resolution: "@segment/analytics.js-integration-amplitude@npm:3.3.3"
@@ -7897,15 +6020,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@swc/helpers@npm:^0.5.0":
-  version: 0.5.15
-  resolution: "@swc/helpers@npm:0.5.15"
-  dependencies:
-    tslib: ^2.8.0
-  checksum: 1a9e0dbb792b2d1e0c914d69c201dbc96af3a0e6e6e8cf5a7f7d6a5d7b0e8b762915cd4447acb6b040e2ecc1ed49822875a7239f99a2d63c96c3c3407fb6fccf
-  languageName: node
-  linkType: hard
-
 "@swc/types@npm:^0.1.21":
   version: 0.1.21
   resolution: "@swc/types@npm:0.1.21"
@@ -8210,13 +6324,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/html-minifier-terser@npm:^6.0.0":
-  version: 6.1.0
-  resolution: "@types/html-minifier-terser@npm:6.1.0"
-  checksum: eb843f6a8d662d44fb18ec61041117734c6aae77aa38df1be3b4712e8e50ffaa35f1e1c92fdd0fde14a5675fecf457abcd0d15a01fae7506c91926176967f452
-  languageName: node
-  linkType: hard
-
 "@types/http-proxy@npm:^1.17.8":
   version: 1.17.11
   resolution: "@types/http-proxy@npm:1.17.11"
@@ -8477,7 +6584,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/react@npm:*, @types/react@npm:^18.0.0":
+"@types/react@npm:*":
   version: 18.2.39
   resolution: "@types/react@npm:18.2.39"
   dependencies:
@@ -10477,13 +8584,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"boolbase@npm:^1.0.0":
-  version: 1.0.0
-  resolution: "boolbase@npm:1.0.0"
-  checksum: 3e25c80ef626c3a3487c73dbfc70ac322ec830666c9ad915d11b701142fab25ec1e63eff2c450c74347acfd2de854ccde865cd79ef4db1683f7c7b046ea43bb0
-  languageName: node
-  linkType: hard
-
 "brace-expansion@npm:^1.1.7":
   version: 1.1.11
   resolution: "brace-expansion@npm:1.1.11"
@@ -10767,16 +8867,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"camel-case@npm:^4.1.2":
-  version: 4.1.2
-  resolution: "camel-case@npm:4.1.2"
-  dependencies:
-    pascal-case: ^3.1.2
-    tslib: ^2.0.3
-  checksum: bcbd25cd253b3cbc69be3f535750137dbf2beb70f093bdc575f73f800acc8443d34fd52ab8f0a2413c34f1e8203139ffc88428d8863e4dfe530cfb257a379ad6
-  languageName: node
-  linkType: hard
-
 "camelcase-keys@npm:^6.2.2":
   version: 6.2.2
   resolution: "camelcase-keys@npm:6.2.2"
@@ -11008,15 +9098,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"clean-css@npm:^5.2.2":
-  version: 5.3.3
-  resolution: "clean-css@npm:5.3.3"
-  dependencies:
-    source-map: ~0.6.0
-  checksum: 941987c14860dd7d346d5cf121a82fd2caf8344160b1565c5387f7ccca4bbcaf885bace961be37c4f4713ce2d8c488dd89483c1add47bb779790edbfdcc79cbc
-  languageName: node
-  linkType: hard
-
 "clean-stack@npm:^2.0.0":
   version: 2.2.0
   resolution: "clean-stack@npm:2.2.0"
@@ -11066,13 +9147,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"client-only@npm:^0.0.1":
-  version: 0.0.1
-  resolution: "client-only@npm:0.0.1"
-  checksum: 0c16bf660dadb90610553c1d8946a7fdfb81d624adea073b8440b7d795d5b5b08beb3c950c6a2cf16279365a3265158a236876d92bce16423c485c322d7dfaf8
-  languageName: node
-  linkType: hard
-
 "cliui@npm:^6.0.0":
   version: 6.0.0
   resolution: "cliui@npm:6.0.0"
@@ -11113,13 +9187,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"clsx@npm:^2.0.0":
-  version: 2.1.1
-  resolution: "clsx@npm:2.1.1"
-  checksum: acd3e1ab9d8a433ecb3cc2f6a05ab95fe50b4a3cfc5ba47abb6cbf3754585fcb87b84e90c822a1f256c4198e3b41c7f6c391577ffc8678ad587fc0976b24fd57
-  languageName: node
-  linkType: hard
-
 "co@npm:^4.6.0":
   version: 4.6.0
   resolution: "co@npm:4.6.0"
@@ -11260,13 +9327,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"commander@npm:^8.3.0":
-  version: 8.3.0
-  resolution: "commander@npm:8.3.0"
-  checksum: 0f82321821fc27b83bd409510bb9deeebcfa799ff0bf5d102128b500b7af22872c0c92cb6a0ebc5a4cf19c6b550fba9cedfa7329d18c6442a625f851377bacf0
-  languageName: node
-  linkType: hard
-
 "commander@npm:^9.1.0, commander@npm:^9.3.0":
   version: 9.3.0
   resolution: "commander@npm:9.3.0"
@@ -11572,82 +9632,29 @@ __metadata:
   dependencies:
     path-key: ^3.1.0
     shebang-command: ^2.0.0
-    which: ^2.0.1
-  checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52
-  languageName: node
-  linkType: hard
-
-"crypt@npm:0.0.2":
-  version: 0.0.2
-  resolution: "crypt@npm:0.0.2"
-  checksum: baf4c7bbe05df656ec230018af8cf7dbe8c14b36b98726939cef008d473f6fe7a4fad906cfea4062c93af516f1550a3f43ceb4d6615329612c6511378ed9fe34
-  languageName: node
-  linkType: hard
-
-"crypto-random-string@npm:^2.0.0":
-  version: 2.0.0
-  resolution: "crypto-random-string@npm:2.0.0"
-  checksum: 0283879f55e7c16fdceacc181f87a0a65c53bc16ffe1d58b9d19a6277adcd71900d02bb2c4843dd55e78c51e30e89b0fec618a7f170ebcc95b33182c28f05fd6
-  languageName: node
-  linkType: hard
-
-"csp_evaluator@npm:1.1.0":
-  version: 1.1.0
-  resolution: "csp_evaluator@npm:1.1.0"
-  checksum: d7f024dfa9d8418c95619ded8f3872339d39f7edd39b70bd095bedd40b06b9f1f8dd639c493090a0b1fafa1dc8d646fc3bd90268d844ed64f1f0874a59c95de5
-  languageName: node
-  linkType: hard
-
-"css-loader@npm:^7.1.2":
-  version: 7.1.2
-  resolution: "css-loader@npm:7.1.2"
-  dependencies:
-    icss-utils: ^5.1.0
-    postcss: ^8.4.33
-    postcss-modules-extract-imports: ^3.1.0
-    postcss-modules-local-by-default: ^4.0.5
-    postcss-modules-scope: ^3.2.0
-    postcss-modules-values: ^4.0.0
-    postcss-value-parser: ^4.2.0
-    semver: ^7.5.4
-  peerDependencies:
-    "@rspack/core": 0.x || 1.x
-    webpack: ^5.27.0
-  peerDependenciesMeta:
-    "@rspack/core":
-      optional: true
-    webpack:
-      optional: true
-  checksum: 15bfd90d778ddab90ee1d04c8c8bcc13ea6c0791d01b52b09d1b1c753b3410f7a7788a510d93726a9878e70b7c1a140f21efdf5c96e1857872107551d3897822
+    which: ^2.0.1
+  checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52
   languageName: node
   linkType: hard
 
-"css-select@npm:^4.1.3":
-  version: 4.3.0
-  resolution: "css-select@npm:4.3.0"
-  dependencies:
-    boolbase: ^1.0.0
-    css-what: ^6.0.1
-    domhandler: ^4.3.1
-    domutils: ^2.8.0
-    nth-check: ^2.0.1
-  checksum: d6202736839194dd7f910320032e7cfc40372f025e4bf21ca5bf6eb0a33264f322f50ba9c0adc35dadd342d3d6fae5ca244779a4873afbfa76561e343f2058e0
+"crypt@npm:0.0.2":
+  version: 0.0.2
+  resolution: "crypt@npm:0.0.2"
+  checksum: baf4c7bbe05df656ec230018af8cf7dbe8c14b36b98726939cef008d473f6fe7a4fad906cfea4062c93af516f1550a3f43ceb4d6615329612c6511378ed9fe34
   languageName: node
   linkType: hard
 
-"css-what@npm:^6.0.1":
-  version: 6.1.0
-  resolution: "css-what@npm:6.1.0"
-  checksum: b975e547e1e90b79625918f84e67db5d33d896e6de846c9b584094e529f0c63e2ab85ee33b9daffd05bff3a146a1916bec664e18bb76dd5f66cbff9fc13b2bbe
+"crypto-random-string@npm:^2.0.0":
+  version: 2.0.0
+  resolution: "crypto-random-string@npm:2.0.0"
+  checksum: 0283879f55e7c16fdceacc181f87a0a65c53bc16ffe1d58b9d19a6277adcd71900d02bb2c4843dd55e78c51e30e89b0fec618a7f170ebcc95b33182c28f05fd6
   languageName: node
   linkType: hard
 
-"cssesc@npm:^3.0.0":
-  version: 3.0.0
-  resolution: "cssesc@npm:3.0.0"
-  bin:
-    cssesc: bin/cssesc
-  checksum: f8c4ababffbc5e2ddf2fa9957dda1ee4af6048e22aeda1869d0d00843223c1b13ad3f5d88b51caa46c994225eacb636b764eb807a8883e2fb6f99b4f4e8c48b2
+"csp_evaluator@npm:1.1.0":
+  version: 1.1.0
+  resolution: "csp_evaluator@npm:1.1.0"
+  checksum: d7f024dfa9d8418c95619ded8f3872339d39f7edd39b70bd095bedd40b06b9f1f8dd639c493090a0b1fafa1dc8d646fc3bd90268d844ed64f1f0874a59c95de5
   languageName: node
   linkType: hard
 
@@ -11892,13 +9899,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"decimal.js@npm:10":
-  version: 10.4.3
-  resolution: "decimal.js@npm:10.4.3"
-  checksum: 796404dcfa9d1dbfdc48870229d57f788b48c21c603c3f6554a1c17c10195fc1024de338b0cf9e1efe0c7c167eeb18f04548979bcc5fdfabebb7cc0ae3287bae
-  languageName: node
-  linkType: hard
-
 "decimal.js@npm:^10.3.1":
   version: 10.3.1
   resolution: "decimal.js@npm:10.3.1"
@@ -12166,33 +10166,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"dom-converter@npm:^0.2.0":
-  version: 0.2.0
-  resolution: "dom-converter@npm:0.2.0"
-  dependencies:
-    utila: ~0.4
-  checksum: ea52fe303f5392e48dea563abef0e6fb3a478b8dbe3c599e99bb5d53981c6c38fc4944e56bb92a8ead6bb989d10b7914722ae11febbd2fd0910e33b9fc4aaa77
-  languageName: node
-  linkType: hard
-
-"dom-serializer@npm:^1.0.1":
-  version: 1.4.1
-  resolution: "dom-serializer@npm:1.4.1"
-  dependencies:
-    domelementtype: ^2.0.1
-    domhandler: ^4.2.0
-    entities: ^2.0.0
-  checksum: fbb0b01f87a8a2d18e6e5a388ad0f7ec4a5c05c06d219377da1abc7bb0f674d804f4a8a94e3f71ff15f6cb7dcfc75704a54b261db672b9b3ab03da6b758b0b22
-  languageName: node
-  linkType: hard
-
-"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0":
-  version: 2.3.0
-  resolution: "domelementtype@npm:2.3.0"
-  checksum: ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6
-  languageName: node
-  linkType: hard
-
 "domexception@npm:^4.0.0":
   version: 4.0.0
   resolution: "domexception@npm:4.0.0"
@@ -12202,15 +10175,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"domhandler@npm:^4.0.0, domhandler@npm:^4.2.0, domhandler@npm:^4.3.1":
-  version: 4.3.1
-  resolution: "domhandler@npm:4.3.1"
-  dependencies:
-    domelementtype: ^2.2.0
-  checksum: 4c665ceed016e1911bf7d1dadc09dc888090b64dee7851cccd2fcf5442747ec39c647bb1cb8c8919f8bbdd0f0c625a6bafeeed4b2d656bbecdbae893f43ffaaa
-  languageName: node
-  linkType: hard
-
 "domify@npm:^1.4.1":
   version: 1.4.1
   resolution: "domify@npm:1.4.1"
@@ -12218,27 +10182,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"domutils@npm:^2.5.2, domutils@npm:^2.8.0":
-  version: 2.8.0
-  resolution: "domutils@npm:2.8.0"
-  dependencies:
-    dom-serializer: ^1.0.1
-    domelementtype: ^2.2.0
-    domhandler: ^4.2.0
-  checksum: abf7434315283e9aadc2a24bac0e00eab07ae4313b40cc239f89d84d7315ebdfd2fb1b5bf750a96bc1b4403d7237c7b2ebf60459be394d625ead4ca89b934391
-  languageName: node
-  linkType: hard
-
-"dot-case@npm:^3.0.4":
-  version: 3.0.4
-  resolution: "dot-case@npm:3.0.4"
-  dependencies:
-    no-case: ^3.0.4
-    tslib: ^2.0.3
-  checksum: a65e3519414856df0228b9f645332f974f2bf5433370f544a681122eab59e66038fc3349b4be1cdc47152779dac71a5864f1ccda2f745e767c46e9c6543b1169
-  languageName: node
-  linkType: hard
-
 "dot-prop@npm:^5.2.0":
   version: 5.3.0
   resolution: "dot-prop@npm:5.3.0"
@@ -12248,27 +10191,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"dotenv-defaults@npm:^2.0.2":
-  version: 2.0.2
-  resolution: "dotenv-defaults@npm:2.0.2"
-  dependencies:
-    dotenv: ^8.2.0
-  checksum: c005960bd048e2189c4799e729c41f362e415e6e0b54313d3f31e853a84b049bf770b25cb21c112c2805bb13a8376edc9de451fb514abf8546392d327c27f6e5
-  languageName: node
-  linkType: hard
-
-"dotenv-webpack@npm:^8.1.0":
-  version: 8.1.0
-  resolution: "dotenv-webpack@npm:8.1.0"
-  dependencies:
-    dotenv-defaults: ^2.0.2
-  peerDependencies:
-    webpack: ^4 || ^5
-  checksum: 9bb53318941b9ea39da7caf4dddd473f0e6bd3b97679fb4b853324e38841d848f8428af784e76d8efeb659f325b9cbcf420422458d635ca6fe72b9057c7c1abe
-  languageName: node
-  linkType: hard
-
-"dotenv@npm:^8.1.0, dotenv@npm:^8.2.0":
+"dotenv@npm:^8.1.0":
   version: 8.6.0
   resolution: "dotenv@npm:8.6.0"
   checksum: 38e902c80b0666ab59e9310a3d24ed237029a7ce34d976796349765ac96b8d769f6df19090f1f471b77a25ca391971efde8a1ea63bb83111bd8bec8e5cc9b2cd
@@ -12455,13 +10378,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"entities@npm:^2.0.0":
-  version: 2.2.0
-  resolution: "entities@npm:2.2.0"
-  checksum: 19010dacaf0912c895ea262b4f6128574f9ccf8d4b3b65c7e8334ad0079b3706376360e28d8843ff50a78aabcb8f08f0a32dbfacdc77e47ed77ca08b713669b3
-  languageName: node
-  linkType: hard
-
 "env-paths@npm:^2.2.0":
   version: 2.2.1
   resolution: "env-paths@npm:2.2.1"
@@ -13871,13 +11787,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"fake-indexeddb@npm:^6.0.0":
-  version: 6.0.0
-  resolution: "fake-indexeddb@npm:6.0.0"
-  checksum: c55bb1f54c1c910a6ca9e18b376ebecc51a29eceb829550d415aa34c2e246730a76dd35dc4fd6d8ae5c5790b9b4f180859e9457bcb73f2d167cc87aa04b6201e
-  languageName: node
-  linkType: hard
-
 "faker@npm:^5.1.0":
   version: 5.5.3
   resolution: "faker@npm:5.5.3"
@@ -15016,56 +12925,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"html-minifier-terser@npm:^6.0.2":
-  version: 6.1.0
-  resolution: "html-minifier-terser@npm:6.1.0"
-  dependencies:
-    camel-case: ^4.1.2
-    clean-css: ^5.2.2
-    commander: ^8.3.0
-    he: ^1.2.0
-    param-case: ^3.0.4
-    relateurl: ^0.2.7
-    terser: ^5.10.0
-  bin:
-    html-minifier-terser: cli.js
-  checksum: ac52c14006476f773204c198b64838477859dc2879490040efab8979c0207424da55d59df7348153f412efa45a0840a1ca3c757bf14767d23a15e3e389d37a93
-  languageName: node
-  linkType: hard
-
-"html-webpack-plugin@npm:^5.6.0":
-  version: 5.6.0
-  resolution: "html-webpack-plugin@npm:5.6.0"
-  dependencies:
-    "@types/html-minifier-terser": ^6.0.0
-    html-minifier-terser: ^6.0.2
-    lodash: ^4.17.21
-    pretty-error: ^4.0.0
-    tapable: ^2.0.0
-  peerDependencies:
-    "@rspack/core": 0.x || 1.x
-    webpack: ^5.20.0
-  peerDependenciesMeta:
-    "@rspack/core":
-      optional: true
-    webpack:
-      optional: true
-  checksum: 32a6e41da538e798fd0be476637d7611a5e8a98a3508f031996e9eb27804dcdc282cb01f847cf5d066f21b49cfb8e21627fcf977ffd0c9bea81cf80e5a65070d
-  languageName: node
-  linkType: hard
-
-"htmlparser2@npm:^6.1.0":
-  version: 6.1.0
-  resolution: "htmlparser2@npm:6.1.0"
-  dependencies:
-    domelementtype: ^2.0.1
-    domhandler: ^4.0.0
-    domutils: ^2.5.2
-    entities: ^2.0.0
-  checksum: 81a7b3d9c3bb9acb568a02fc9b1b81ffbfa55eae7f1c41ae0bf840006d1dbf54cb3aa245b2553e2c94db674840a9f0fdad7027c9a9d01a062065314039058c4e
-  languageName: node
-  linkType: hard
-
 "http-cache-semantics@npm:^4.1.0":
   version: 4.1.1
   resolution: "http-cache-semantics@npm:4.1.1"
@@ -15266,22 +13125,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"icss-utils@npm:^5.0.0, icss-utils@npm:^5.1.0":
-  version: 5.1.0
-  resolution: "icss-utils@npm:5.1.0"
-  peerDependencies:
-    postcss: ^8.1.0
-  checksum: 5c324d283552b1269cfc13a503aaaa172a280f914e5b81544f3803bc6f06a3b585fb79f66f7c771a2c052db7982c18bf92d001e3b47282e3abbbb4c4cc488d68
-  languageName: node
-  linkType: hard
-
-"idb@npm:^8.0.0":
-  version: 8.0.0
-  resolution: "idb@npm:8.0.0"
-  checksum: a9c6176c176dc1a73520ae906d33fcda8a6f6068cf64027e196763d4ad70b088b7141650ed68f3604e0f0ccd1a123f6b8a435ba5e4514f42ada3460c23b6747a
-  languageName: node
-  linkType: hard
-
 "ieee754@npm:1.1.13, ieee754@npm:^1.1.4":
   version: 1.1.13
   resolution: "ieee754@npm:1.1.13"
@@ -15452,18 +13295,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"intl-messageformat@npm:^10.1.0":
-  version: 10.7.10
-  resolution: "intl-messageformat@npm:10.7.10"
-  dependencies:
-    "@formatjs/ecma402-abstract": 2.3.1
-    "@formatjs/fast-memoize": 2.2.5
-    "@formatjs/icu-messageformat-parser": 2.9.7
-    tslib: 2
-  checksum: ff929fbb1883f6598e0dc0e0ff341a7feb244fccc844ddd6a9045b4a9ced6b3f357de73547d050d20390496a7b283811741ff75c267e81063860258a4914e80e
-  languageName: node
-  linkType: hard
-
 "intl-messageformat@npm:^4.4.0":
   version: 4.4.0
   resolution: "intl-messageformat@npm:4.4.0"
@@ -17518,15 +15349,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lower-case@npm:^2.0.2":
-  version: 2.0.2
-  resolution: "lower-case@npm:2.0.2"
-  dependencies:
-    tslib: ^2.0.3
-  checksum: 83a0a5f159ad7614bee8bf976b96275f3954335a84fad2696927f609ddae902802c4f3312d86668722e668bef41400254807e1d3a7f2e8c3eede79691aa1f010
-  languageName: node
-  linkType: hard
-
 "lru-cache@npm:^10.2.0":
   version: 10.4.3
   resolution: "lru-cache@npm:10.4.3"
@@ -18221,7 +16043,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"nanoid@npm:^3.3.4, nanoid@npm:^3.3.6, nanoid@npm:^3.3.7":
+"nanoid@npm:^3.3.4, nanoid@npm:^3.3.6":
   version: 3.3.7
   resolution: "nanoid@npm:3.3.7"
   bin:
@@ -18345,16 +16167,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"no-case@npm:^3.0.4":
-  version: 3.0.4
-  resolution: "no-case@npm:3.0.4"
-  dependencies:
-    lower-case: ^2.0.2
-    tslib: ^2.0.3
-  checksum: 0b2ebc113dfcf737d48dde49cfebf3ad2d82a8c3188e7100c6f375e30eafbef9e9124aadc3becef237b042fd5eb0aad2fd78669c20972d045bbe7fea8ba0be5c
-  languageName: node
-  linkType: hard
-
 "nock@npm:^13.3.0":
   version: 13.3.0
   resolution: "nock@npm:13.3.0"
@@ -18510,15 +16322,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"nth-check@npm:^2.0.1":
-  version: 2.1.1
-  resolution: "nth-check@npm:2.1.1"
-  dependencies:
-    boolbase: ^1.0.0
-  checksum: 5afc3dafcd1573b08877ca8e6148c52abd565f1d06b1eb08caf982e3fa289a82f2cae697ffb55b5021e146d60443f1590a5d6b944844e944714a5b549675bcd3
-  languageName: node
-  linkType: hard
-
 "nwsapi@npm:^2.2.0":
   version: 2.2.0
   resolution: "nwsapi@npm:2.2.0"
@@ -18919,16 +16722,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"param-case@npm:^3.0.4":
-  version: 3.0.4
-  resolution: "param-case@npm:3.0.4"
-  dependencies:
-    dot-case: ^3.0.4
-    tslib: ^2.0.3
-  checksum: b34227fd0f794e078776eb3aa6247442056cb47761e9cd2c4c881c86d84c64205f6a56ef0d70b41ee7d77da02c3f4ed2f88e3896a8fefe08bdfb4deca037c687
-  languageName: node
-  linkType: hard
-
 "parent-module@npm:^1.0.0":
   version: 1.0.1
   resolution: "parent-module@npm:1.0.1"
@@ -18988,16 +16781,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"pascal-case@npm:^3.1.2":
-  version: 3.1.2
-  resolution: "pascal-case@npm:3.1.2"
-  dependencies:
-    no-case: ^3.0.4
-    tslib: ^2.0.3
-  checksum: ba98bfd595fc91ef3d30f4243b1aee2f6ec41c53b4546bfa3039487c367abaa182471dcfc830a1f9e1a0df00c14a370514fa2b3a1aacc68b15a460c31116873e
-  languageName: node
-  linkType: hard
-
 "path-exists@npm:^4.0.0":
   version: 4.0.0
   resolution: "path-exists@npm:4.0.0"
@@ -19236,67 +17019,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss-modules-extract-imports@npm:^3.1.0":
-  version: 3.1.0
-  resolution: "postcss-modules-extract-imports@npm:3.1.0"
-  peerDependencies:
-    postcss: ^8.1.0
-  checksum: b9192e0f4fb3d19431558be6f8af7ca45fc92baaad9b2778d1732a5880cd25c3df2074ce5484ae491e224f0d21345ffc2d419bd51c25b019af76d7a7af88c17f
-  languageName: node
-  linkType: hard
-
-"postcss-modules-local-by-default@npm:^4.0.5":
-  version: 4.0.5
-  resolution: "postcss-modules-local-by-default@npm:4.0.5"
-  dependencies:
-    icss-utils: ^5.0.0
-    postcss-selector-parser: ^6.0.2
-    postcss-value-parser: ^4.1.0
-  peerDependencies:
-    postcss: ^8.1.0
-  checksum: ca9b01f4a0a3dfb33e016299e2dfb7e85c3123292f7aec2efc0c6771b9955648598bfb4c1561f7ee9732fb27fb073681233661b32eef98baab43743f96735452
-  languageName: node
-  linkType: hard
-
-"postcss-modules-scope@npm:^3.2.0":
-  version: 3.2.0
-  resolution: "postcss-modules-scope@npm:3.2.0"
-  dependencies:
-    postcss-selector-parser: ^6.0.4
-  peerDependencies:
-    postcss: ^8.1.0
-  checksum: 2ffe7e98c1fa993192a39c8dd8ade93fc4f59fbd1336ce34fcedaee0ee3bafb29e2e23fb49189256895b30e4f21af661c6a6a16ef7b17ae2c859301e4a4459ae
-  languageName: node
-  linkType: hard
-
-"postcss-modules-values@npm:^4.0.0":
-  version: 4.0.0
-  resolution: "postcss-modules-values@npm:4.0.0"
-  dependencies:
-    icss-utils: ^5.0.0
-  peerDependencies:
-    postcss: ^8.1.0
-  checksum: f7f2cdf14a575b60e919ad5ea52fed48da46fe80db2733318d71d523fc87db66c835814940d7d05b5746b0426e44661c707f09bdb83592c16aea06e859409db6
-  languageName: node
-  linkType: hard
-
-"postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4":
-  version: 6.1.0
-  resolution: "postcss-selector-parser@npm:6.1.0"
-  dependencies:
-    cssesc: ^3.0.0
-    util-deprecate: ^1.0.2
-  checksum: 449f614e6706421be307d8638183c61ba45bc3b460fe3815df8971dbb4d59c4087181940d879daee4a7a2daf3d86e915db1cce0c006dd68ca75b4087079273bd
-  languageName: node
-  linkType: hard
-
-"postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0":
-  version: 4.2.0
-  resolution: "postcss-value-parser@npm:4.2.0"
-  checksum: 819ffab0c9d51cf0acbabf8996dffbfafbafa57afc0e4c98db88b67f2094cb44488758f06e5da95d7036f19556a4a732525e84289a425f4f6fd8e412a9d7442f
-  languageName: node
-  linkType: hard
-
 "postcss@npm:8.4.14":
   version: 8.4.14
   resolution: "postcss@npm:8.4.14"
@@ -19319,17 +17041,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"postcss@npm:^8.4.33":
-  version: 8.4.38
-  resolution: "postcss@npm:8.4.38"
-  dependencies:
-    nanoid: ^3.3.7
-    picocolors: ^1.0.0
-    source-map-js: ^1.2.0
-  checksum: 649f9e60a763ca4b5a7bbec446a069edf07f057f6d780a5a0070576b841538d1ecf7dd888f2fbfd1f76200e26c969e405aeeae66332e6927dbdc8bdcb90b9451
-  languageName: node
-  linkType: hard
-
 "preferred-pm@npm:^3.0.0":
   version: 3.0.3
   resolution: "preferred-pm@npm:3.0.3"
@@ -19390,16 +17101,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"pretty-error@npm:^4.0.0":
-  version: 4.0.0
-  resolution: "pretty-error@npm:4.0.0"
-  dependencies:
-    lodash: ^4.17.20
-    renderkid: ^3.0.0
-  checksum: a5b9137365690104ded6947dca2e33360bf55e62a4acd91b1b0d7baa3970e43754c628cc9e16eafbdd4e8f8bcb260a5865475d4fc17c3106ff2d61db4e72cdf3
-  languageName: node
-  linkType: hard
-
 "pretty-format@npm:^28.1.1":
   version: 28.1.1
   resolution: "pretty-format@npm:28.1.1"
@@ -19738,99 +17439,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-aria-components@npm:^1.5.0":
-  version: 1.5.0
-  resolution: "react-aria-components@npm:1.5.0"
-  dependencies:
-    "@internationalized/date": ^3.6.0
-    "@internationalized/string": ^3.2.5
-    "@react-aria/collections": 3.0.0-alpha.6
-    "@react-aria/color": ^3.0.2
-    "@react-aria/disclosure": ^3.0.0
-    "@react-aria/dnd": ^3.8.0
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/live-announcer": ^3.4.1
-    "@react-aria/menu": ^3.16.0
-    "@react-aria/toolbar": 3.0.0-beta.11
-    "@react-aria/tree": 3.0.0-beta.2
-    "@react-aria/utils": ^3.26.0
-    "@react-aria/virtualizer": ^4.1.0
-    "@react-stately/color": ^3.8.1
-    "@react-stately/disclosure": ^3.0.0
-    "@react-stately/layout": ^4.1.0
-    "@react-stately/menu": ^3.9.0
-    "@react-stately/selection": ^3.18.0
-    "@react-stately/table": ^3.13.0
-    "@react-stately/utils": ^3.10.5
-    "@react-stately/virtualizer": ^4.2.0
-    "@react-types/color": ^3.0.1
-    "@react-types/form": ^3.7.8
-    "@react-types/grid": ^3.2.10
-    "@react-types/shared": ^3.26.0
-    "@react-types/table": ^3.10.3
-    "@swc/helpers": ^0.5.0
-    client-only: ^0.0.1
-    react-aria: ^3.36.0
-    react-stately: ^3.34.0
-    use-sync-external-store: ^1.2.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: d785a35c5506de0521f2a3f9938980a47111fd6ce28244dee3107e82ef4acb855177a657853a759977eafa22ad3ca45d626b8478eaf9182327ae1e3bfdc1364b
-  languageName: node
-  linkType: hard
-
-"react-aria@npm:^3.36.0":
-  version: 3.36.0
-  resolution: "react-aria@npm:3.36.0"
-  dependencies:
-    "@internationalized/string": ^3.2.5
-    "@react-aria/breadcrumbs": ^3.5.19
-    "@react-aria/button": ^3.11.0
-    "@react-aria/calendar": ^3.6.0
-    "@react-aria/checkbox": ^3.15.0
-    "@react-aria/color": ^3.0.2
-    "@react-aria/combobox": ^3.11.0
-    "@react-aria/datepicker": ^3.12.0
-    "@react-aria/dialog": ^3.5.20
-    "@react-aria/disclosure": ^3.0.0
-    "@react-aria/dnd": ^3.8.0
-    "@react-aria/focus": ^3.19.0
-    "@react-aria/gridlist": ^3.10.0
-    "@react-aria/i18n": ^3.12.4
-    "@react-aria/interactions": ^3.22.5
-    "@react-aria/label": ^3.7.13
-    "@react-aria/link": ^3.7.7
-    "@react-aria/listbox": ^3.13.6
-    "@react-aria/menu": ^3.16.0
-    "@react-aria/meter": ^3.4.18
-    "@react-aria/numberfield": ^3.11.9
-    "@react-aria/overlays": ^3.24.0
-    "@react-aria/progress": ^3.4.18
-    "@react-aria/radio": ^3.10.10
-    "@react-aria/searchfield": ^3.7.11
-    "@react-aria/select": ^3.15.0
-    "@react-aria/selection": ^3.21.0
-    "@react-aria/separator": ^3.4.4
-    "@react-aria/slider": ^3.7.14
-    "@react-aria/ssr": ^3.9.7
-    "@react-aria/switch": ^3.6.10
-    "@react-aria/table": ^3.16.0
-    "@react-aria/tabs": ^3.9.8
-    "@react-aria/tag": ^3.4.8
-    "@react-aria/textfield": ^3.15.0
-    "@react-aria/tooltip": ^3.7.10
-    "@react-aria/utils": ^3.26.0
-    "@react-aria/visually-hidden": ^3.8.18
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-    react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 788b459f4bb9977bbd7817fec52cb303640ba42ea7f29dd43a563db644a16f93fee8df25f3d7bef9ae877c4ca4b73aa3c16636013999ad9f8448d42aa860324d
-  languageName: node
-  linkType: hard
-
 "react-base16-styling@npm:^0.8.0":
   version: 0.8.2
   resolution: "react-base16-styling@npm:0.8.2"
@@ -19870,15 +17478,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-hook-form@npm:^7.54.2":
-  version: 7.54.2
-  resolution: "react-hook-form@npm:7.54.2"
-  peerDependencies:
-    react: ^16.8.0 || ^17 || ^18 || ^19
-  checksum: 49a867ece9894dca82f6552e5eefd012b7db962c56a7543f9275ae0b6ec202d549973c3694e7f97436afc2396549cb8fc8777241dd660b71793547aa9c8e5686
-  languageName: node
-  linkType: hard
-
 "react-is@npm:^16.13.1":
   version: 16.13.1
   resolution: "react-is@npm:16.13.1"
@@ -19921,30 +17520,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-router-dom@npm:^6.23.1":
-  version: 6.23.1
-  resolution: "react-router-dom@npm:6.23.1"
-  dependencies:
-    "@remix-run/router": 1.16.1
-    react-router: 6.23.1
-  peerDependencies:
-    react: ">=16.8"
-    react-dom: ">=16.8"
-  checksum: e87b5cf85019496f499286d466a4ad9cf5efe729f1420502fc5d16093d525462803253538418ea5b0da7ab5671a16caefee67848b373008e567629c2d667dc44
-  languageName: node
-  linkType: hard
-
-"react-router@npm:6.23.1":
-  version: 6.23.1
-  resolution: "react-router@npm:6.23.1"
-  dependencies:
-    "@remix-run/router": 1.16.1
-  peerDependencies:
-    react: ">=16.8"
-  checksum: d5d43ccb908a95d2b7345f2a13315c38bf094e25bcf97d5a6c3f353b1ea88602de15726c3570cd7f07c53b19a3519af2b6739bf6929ec355012795611d739cff
-  languageName: node
-  linkType: hard
-
 "react-simple-code-editor@npm:^0.11.0":
   version: 0.11.3
   resolution: "react-simple-code-editor@npm:0.11.3"
@@ -19955,41 +17530,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-stately@npm:^3.34.0":
-  version: 3.34.0
-  resolution: "react-stately@npm:3.34.0"
-  dependencies:
-    "@react-stately/calendar": ^3.6.0
-    "@react-stately/checkbox": ^3.6.10
-    "@react-stately/collections": ^3.12.0
-    "@react-stately/color": ^3.8.1
-    "@react-stately/combobox": ^3.10.1
-    "@react-stately/data": ^3.12.0
-    "@react-stately/datepicker": ^3.11.0
-    "@react-stately/disclosure": ^3.0.0
-    "@react-stately/dnd": ^3.5.0
-    "@react-stately/form": ^3.1.0
-    "@react-stately/list": ^3.11.1
-    "@react-stately/menu": ^3.9.0
-    "@react-stately/numberfield": ^3.9.8
-    "@react-stately/overlays": ^3.6.12
-    "@react-stately/radio": ^3.10.9
-    "@react-stately/searchfield": ^3.5.8
-    "@react-stately/select": ^3.6.9
-    "@react-stately/selection": ^3.18.0
-    "@react-stately/slider": ^3.6.0
-    "@react-stately/table": ^3.13.0
-    "@react-stately/tabs": ^3.7.0
-    "@react-stately/toggle": ^3.8.0
-    "@react-stately/tooltip": ^3.5.0
-    "@react-stately/tree": ^3.8.6
-    "@react-types/shared": ^3.26.0
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
-  checksum: 6e888c8d0a6d7b8234e6170fab0eebb9e72b163c17dd5111198c9eea5d3e1d7c31a1080f174b45ca30920ff3ebf647250317b8697e0b3a9507a01c211e323c29
-  languageName: node
-  linkType: hard
-
 "react@npm:^17.0.2":
   version: 17.0.2
   resolution: "react@npm:17.0.2"
@@ -20260,13 +17800,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"relateurl@npm:^0.2.7":
-  version: 0.2.7
-  resolution: "relateurl@npm:0.2.7"
-  checksum: 5891e792eae1dfc3da91c6fda76d6c3de0333a60aa5ad848982ebb6dccaa06e86385fb1235a1582c680a3d445d31be01c6bfc0804ebbcab5aaf53fa856fde6b6
-  languageName: node
-  linkType: hard
-
 "remove-trailing-slash@npm:^0.1.0":
   version: 0.1.1
   resolution: "remove-trailing-slash@npm:0.1.1"
@@ -20274,19 +17807,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"renderkid@npm:^3.0.0":
-  version: 3.0.0
-  resolution: "renderkid@npm:3.0.0"
-  dependencies:
-    css-select: ^4.1.3
-    dom-converter: ^0.2.0
-    htmlparser2: ^6.1.0
-    lodash: ^4.17.21
-    strip-ansi: ^6.0.1
-  checksum: 77162b62d6f33ab81f337c39efce0439ff0d1f6d441e29c35183151f83041c7850774fb904da163d6c844264d440d10557714e6daa0b19e4561a5cd4ef305d41
-  languageName: node
-  linkType: hard
-
 "require-directory@npm:^2.1.1":
   version: 2.1.1
   resolution: "require-directory@npm:2.1.1"
@@ -21423,13 +18943,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"source-map-js@npm:^1.2.0":
-  version: 1.2.0
-  resolution: "source-map-js@npm:1.2.0"
-  checksum: 791a43306d9223792e84293b00458bf102a8946e7188f3db0e4e22d8d530b5f80a4ce468eb5ec0bf585443ad55ebbd630bf379c98db0b1f317fd902500217f97
-  languageName: node
-  linkType: hard
-
 "source-map-loader@npm:^3.0.1":
   version: 3.0.2
   resolution: "source-map-loader@npm:3.0.2"
@@ -21463,7 +18976,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"source-map@npm:0.6.1, source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.0, source-map@npm:~0.6.1":
+"source-map@npm:0.6.1, source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.1":
   version: 0.6.1
   resolution: "source-map@npm:0.6.1"
   checksum: 59ce8640cf3f3124f64ac289012c2b8bd377c238e316fb323ea22fbfe83da07d81e000071d7242cad7a23cd91c7de98e4df8830ec3f133cb6133a5f6e9f67bc2
@@ -21940,15 +19453,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"style-loader@npm:^4.0.0":
-  version: 4.0.0
-  resolution: "style-loader@npm:4.0.0"
-  peerDependencies:
-    webpack: ^5.27.0
-  checksum: 0b751b4cc8394a2fe1df6194bb2f6dd68e859e36f22030994bb7b5220f24f9efb5705e78b2442226e6fa4c90f74b397529c7eb0a1d7326fb016e1e140e90151c
-  languageName: node
-  linkType: hard
-
 "styled-jsx@npm:5.0.7":
   version: 5.0.7
   resolution: "styled-jsx@npm:5.0.7"
@@ -22031,13 +19535,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"tapable@npm:^2.0.0":
-  version: 2.2.1
-  resolution: "tapable@npm:2.2.1"
-  checksum: 3b7a1b4d86fa940aad46d9e73d1e8739335efd4c48322cb37d073eb6f80f5281889bf0320c6d8ffcfa1a0dd5bfdbd0f9d037e252ef972aca595330538aac4d51
-  languageName: node
-  linkType: hard
-
 "tapable@npm:^2.1.1, tapable@npm:^2.2.0":
   version: 2.2.0
   resolution: "tapable@npm:2.2.0"
@@ -22173,20 +19670,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"terser@npm:^5.10.0":
-  version: 5.31.0
-  resolution: "terser@npm:5.31.0"
-  dependencies:
-    "@jridgewell/source-map": ^0.3.3
-    acorn: ^8.8.2
-    commander: ^2.20.0
-    source-map-support: ~0.5.20
-  bin:
-    terser: bin/terser
-  checksum: 48f14229618866bba8a9464e9d0e7fdcb6b6488b3a6c4690fcf4d48df65bf45959d5ae8c02f1a0b3f3dd035a9ae340b715e1e547645b112dc3963daa3564699a
-  languageName: node
-  linkType: hard
-
 "terser@npm:^5.16.8":
   version: 5.19.2
   resolution: "terser@npm:5.19.2"
@@ -22553,13 +20036,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"tslib@npm:2, tslib@npm:^2.8.0":
-  version: 2.8.1
-  resolution: "tslib@npm:2.8.1"
-  checksum: e4aba30e632b8c8902b47587fd13345e2827fa639e7c3121074d5ee0880723282411a8838f830b55100cbe4517672f84a2472667d355b81e8af165a55dc6203a
-  languageName: node
-  linkType: hard
-
 "tslib@npm:^1.8.1, tslib@npm:^1.9.0":
   version: 1.13.0
   resolution: "tslib@npm:1.13.0"
@@ -22574,13 +20050,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"tslib@npm:^2.0.3, tslib@npm:^2.2.0, tslib@npm:^2.4.0":
-  version: 2.6.2
-  resolution: "tslib@npm:2.6.2"
-  checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad
-  languageName: node
-  linkType: hard
-
 "tslib@npm:^2.1.0":
   version: 2.4.0
   resolution: "tslib@npm:2.4.0"
@@ -22588,6 +20057,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"tslib@npm:^2.2.0, tslib@npm:^2.4.0":
+  version: 2.6.2
+  resolution: "tslib@npm:2.6.2"
+  checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad
+  languageName: node
+  linkType: hard
+
 "tslib@npm:^2.4.1":
   version: 2.4.1
   resolution: "tslib@npm:2.4.1"
@@ -23169,29 +20645,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"use-sync-external-store@npm:^1.2.0":
-  version: 1.4.0
-  resolution: "use-sync-external-store@npm:1.4.0"
-  peerDependencies:
-    react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
-  checksum: dc3843a1b59ac8bd01417bd79498d4c688d5df8bf4801be50008ef4bfaacb349058c0b1605b5b43c828e0a2d62722d7e861573b3f31cea77a7f23e8b0fc2f7e3
-  languageName: node
-  linkType: hard
-
-"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1":
+"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1":
   version: 1.0.2
   resolution: "util-deprecate@npm:1.0.2"
   checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2
   languageName: node
   linkType: hard
 
-"utila@npm:~0.4":
-  version: 0.4.0
-  resolution: "utila@npm:0.4.0"
-  checksum: 97ffd3bd2bb80c773429d3fb8396469115cd190dded1e733f190d8b602bd0a1bcd6216b7ce3c4395ee3c79e3c879c19d268dbaae3093564cb169ad1212d436f4
-  languageName: node
-  linkType: hard
-
 "utils-merge@npm:1.0.1":
   version: 1.0.1
   resolution: "utils-merge@npm:1.0.1"