├── .buildkite ├── Dockerfile.agent ├── Makefile ├── Readme.md └── pipeline.yml ├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .eslintrc.isomorphic.js ├── .eslintrc.js ├── .gitattributes ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE ├── architecture.png └── workflows │ ├── ci.yml │ ├── create-jira.yml │ ├── md-link-check.config.json │ ├── md-link-check.yml │ └── release-creator.yml ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .nvmrc ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-constraints.cjs │ │ ├── plugin-interactive-tools.cjs │ │ ├── plugin-typescript.cjs │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-3.4.1.cjs ├── .yarnrc.yml ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── README.md ├── RELEASING.md ├── codecov.yml ├── constraints.pro ├── example.png ├── img └── twilio-segment-logo-2x.png ├── jest.config.js ├── meta-tests └── check-dts.ts ├── package.json ├── packages ├── browser-integration-tests │ ├── .eslintrc.js │ ├── .lintstagedrc.js │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── playwright.config.ts │ ├── src │ │ ├── custom-global-key.test.ts │ │ ├── helpers │ │ │ ├── extract-writekey.ts │ │ │ ├── get-persisted-items.ts │ │ │ └── standalone-mock.ts │ │ ├── index.test.ts │ │ ├── segment-retries.test.ts │ │ └── shims.d.ts │ ├── standalone-custom-key.html │ ├── standalone.html │ └── tsconfig.json ├── browser │ ├── .eslintrc.js │ ├── .gitignore │ ├── .lintstagedrc.js │ ├── ARCHITECTURE.md │ ├── CHANGELOG.md │ ├── LICENSE.MD │ ├── Makefile │ ├── README.md │ ├── e2e-tests │ │ ├── local-server.ts │ │ └── performance │ │ │ └── ajs-perf-browser.test.ts │ ├── jest.config.js │ ├── jest.setup.js │ ├── package.json │ ├── qa │ │ ├── README.md │ │ ├── __fixtures__ │ │ │ └── snippets.ts │ │ ├── __tests__ │ │ │ ├── backwards-compatibility.test.ts │ │ │ ├── destinations.test.ts │ │ │ └── smoke.test.ts │ │ └── lib │ │ │ ├── benchmark.ts │ │ │ ├── browser.ts │ │ │ ├── jest-reporter.js │ │ │ ├── runner.ts │ │ │ ├── schema.ts │ │ │ ├── server.ts │ │ │ └── stats.ts │ ├── scripts │ │ ├── build-prep.sh │ │ ├── ci.sh │ │ ├── release.js │ │ ├── release.sh │ │ ├── run.sh │ │ ├── umd-diff.sh │ │ └── vendor │ │ │ ├── README.md │ │ │ ├── helpers.js │ │ │ ├── run.js │ │ │ └── webpack.config.vendor.js │ ├── src │ │ ├── browser │ │ │ ├── __tests__ │ │ │ │ ├── analytics-lazy-init.integration.test.ts │ │ │ │ ├── analytics-pre-init.integration.test.ts │ │ │ │ ├── anon-id-and-reset.integration.test.ts │ │ │ │ ├── cdn.test.ts │ │ │ │ ├── csp-detection.test.ts │ │ │ │ ├── inspector.integration.test.ts │ │ │ │ ├── integration.test.ts │ │ │ │ ├── integrations.integration.test.ts │ │ │ │ ├── page-enrichment.integration.test.ts │ │ │ │ ├── query-string.integration.test.ts │ │ │ │ ├── standalone-analytics.test.ts │ │ │ │ ├── standalone-errors.test.ts │ │ │ │ ├── standalone.test.ts │ │ │ │ ├── typedef-tests │ │ │ │ │ └── analytics-browser.ts │ │ │ │ └── update-cdn-settings.test.ts │ │ │ ├── browser-umd.ts │ │ │ ├── index.ts │ │ │ ├── settings.ts │ │ │ ├── standalone-analytics.ts │ │ │ ├── standalone-interface.ts │ │ │ └── standalone.ts │ │ ├── core │ │ │ ├── __tests__ │ │ │ │ ├── track-form.test.ts │ │ │ │ └── track-link.test.ts │ │ │ ├── analytics │ │ │ │ ├── __tests__ │ │ │ │ │ ├── analytics.test.ts │ │ │ │ │ ├── null-analytics.test.ts │ │ │ │ │ └── test-plugins.ts │ │ │ │ ├── index.ts │ │ │ │ └── interfaces.ts │ │ │ ├── arguments-resolver │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ └── index.ts │ │ │ ├── auto-track.ts │ │ │ ├── buffer │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ └── index.ts │ │ │ ├── callback │ │ │ │ └── index.ts │ │ │ ├── connection │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ └── index.ts │ │ │ ├── constants │ │ │ │ └── index.ts │ │ │ ├── context │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ └── index.ts │ │ │ ├── environment │ │ │ │ └── index.ts │ │ │ ├── events │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ └── interfaces.ts │ │ │ ├── inspector │ │ │ │ └── index.ts │ │ │ ├── page │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ ├── add-page-context.ts │ │ │ │ └── index.ts │ │ │ ├── plugin │ │ │ │ └── index.ts │ │ │ ├── query-string │ │ │ │ ├── __tests__ │ │ │ │ │ ├── gracefulDecodeURIComponent.test.ts │ │ │ │ │ ├── index.test.ts │ │ │ │ │ ├── pickPrefix.test.ts │ │ │ │ │ └── useQueryString.test.ts │ │ │ │ ├── gracefulDecodeURIComponent.ts │ │ │ │ ├── index.ts │ │ │ │ └── pickPrefix.ts │ │ │ ├── queue │ │ │ │ ├── __tests__ │ │ │ │ │ └── event-queue.test.ts │ │ │ │ └── event-queue.ts │ │ │ ├── stats │ │ │ │ ├── __tests__ │ │ │ │ │ ├── index.test.ts │ │ │ │ │ └── remote-metrics.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── metric-helpers.ts │ │ │ │ └── remote-metrics.ts │ │ │ ├── storage │ │ │ │ ├── __tests__ │ │ │ │ │ ├── cookieStorage.test.ts │ │ │ │ │ ├── localStorage.test.ts │ │ │ │ │ ├── test-helpers.ts │ │ │ │ │ └── universalStorage.test.ts │ │ │ │ ├── cookieStorage.ts │ │ │ │ ├── index.ts │ │ │ │ ├── localStorage.ts │ │ │ │ ├── memoryStorage.ts │ │ │ │ ├── settings.ts │ │ │ │ ├── types.ts │ │ │ │ └── universalStorage.ts │ │ │ └── user │ │ │ │ ├── __tests__ │ │ │ │ ├── index.test.ts │ │ │ │ └── tld.test.ts │ │ │ │ ├── index.ts │ │ │ │ └── tld.ts │ │ ├── generated │ │ │ ├── __tests__ │ │ │ │ └── version.test.ts │ │ │ └── version.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── __tests__ │ │ │ │ ├── embedded-write-key.test.ts │ │ │ │ ├── fetch.test.ts │ │ │ │ ├── get-process-env.test.ts │ │ │ │ ├── group-by.test.ts │ │ │ │ ├── is-plan-event-enabled.test.ts │ │ │ │ ├── is-thenable.test.ts │ │ │ │ ├── load-script.test.ts │ │ │ │ ├── merged-options.test.ts │ │ │ │ ├── on-page-change.test.ts │ │ │ │ ├── parse-cdn.test.ts │ │ │ │ ├── pick.test.ts │ │ │ │ └── pick.typedef.ts │ │ │ ├── bind-all.ts │ │ │ ├── browser-polyfill.ts │ │ │ ├── client-hints │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ └── interfaces.ts │ │ │ ├── csp-detection.ts │ │ │ ├── embedded-write-key.ts │ │ │ ├── fetch.ts │ │ │ ├── get-global.ts │ │ │ ├── get-process-env.ts │ │ │ ├── global-analytics-helper.ts │ │ │ ├── group-by.ts │ │ │ ├── is-plan-event-enabled.ts │ │ │ ├── is-thenable.ts │ │ │ ├── load-script.ts │ │ │ ├── merged-options.ts │ │ │ ├── on-page-change.ts │ │ │ ├── p-while.ts │ │ │ ├── parse-cdn.ts │ │ │ ├── pick.ts │ │ │ ├── priority-queue │ │ │ │ ├── __tests__ │ │ │ │ │ ├── backoff.test.ts │ │ │ │ │ ├── index.test.ts │ │ │ │ │ └── persisted.test.ts │ │ │ │ ├── backoff.ts │ │ │ │ ├── index.ts │ │ │ │ └── persisted.ts │ │ │ ├── sleep.ts │ │ │ ├── to-facade.ts │ │ │ └── version-type.ts │ │ ├── node │ │ │ ├── __tests__ │ │ │ │ └── node-integration.test.ts │ │ │ ├── index.ts │ │ │ └── node.browser.ts │ │ ├── plugins │ │ │ ├── ajs-destination │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── loader.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── analytics-node │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ └── index.ts │ │ │ ├── env-enrichment │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ └── index.ts │ │ │ ├── legacy-video-plugins │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ └── index.ts │ │ │ ├── middleware │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ └── index.ts │ │ │ ├── remote-loader │ │ │ │ ├── __tests__ │ │ │ │ │ ├── action-destination.test.ts │ │ │ │ │ └── index.test.ts │ │ │ │ └── index.ts │ │ │ ├── remote-middleware │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ └── index.ts │ │ │ ├── routing-middleware │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ └── index.ts │ │ │ ├── schema-filter │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.test.ts │ │ │ │ └── index.ts │ │ │ └── segmentio │ │ │ │ ├── __tests__ │ │ │ │ ├── batched-dispatcher.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── normalize.test.ts │ │ │ │ └── retries.test.ts │ │ │ │ ├── batched-dispatcher.ts │ │ │ │ ├── fetch-dispatcher.ts │ │ │ │ ├── index.ts │ │ │ │ ├── normalize.ts │ │ │ │ ├── ratelimit-error.ts │ │ │ │ ├── schedule-flush.ts │ │ │ │ └── shared-dispatcher.ts │ │ ├── test-helpers │ │ │ ├── browser-storage.ts │ │ │ ├── factories.ts │ │ │ ├── fetch-parse.ts │ │ │ ├── fixtures │ │ │ │ ├── cdn-settings.ts │ │ │ │ ├── classic-destination.ts │ │ │ │ ├── client-hints.ts │ │ │ │ ├── create-fetch-method.ts │ │ │ │ ├── index.ts │ │ │ │ └── page-context.ts │ │ │ ├── helpers.ts │ │ │ ├── test-writekeys.ts │ │ │ └── type-assertions.ts │ │ ├── tester │ │ │ ├── __fixtures__ │ │ │ │ ├── index.html │ │ │ │ └── segment-snippet.ts │ │ │ ├── ajs-perf.ts │ │ │ ├── ajs-tester.ts │ │ │ └── server.js │ │ └── vendor │ │ │ └── tsub │ │ │ ├── __tests__ │ │ │ └── index.test.ts │ │ │ ├── tsub.js.LICENSE.txt │ │ │ ├── tsub.ts │ │ │ └── types.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── webpack.config.js ├── config-tsup │ ├── config.js │ └── package.json ├── config-webpack │ ├── README.md │ ├── package.json │ └── webpack.config.common.js ├── config │ ├── package.json │ └── src │ │ ├── index.js │ │ ├── jest │ │ ├── config.js │ │ └── get-module-map.js │ │ └── lint-staged │ │ └── config.js ├── consent │ ├── README.md │ ├── consent-tools-integration-tests │ │ ├── .eslintrc.js │ │ ├── .gitignore │ │ ├── .lintstagedrc.js │ │ ├── README.md │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── playwright.global-setup.ts │ │ ├── public │ │ │ ├── consent-tools-onetrust.html │ │ │ ├── consent-tools-vanilla-opt-in.html │ │ │ └── consent-tools-vanilla-opt-out.html │ │ ├── src │ │ │ ├── page-bundles │ │ │ │ ├── consent-tools-vanilla-opt-in │ │ │ │ │ └── index.ts │ │ │ │ ├── consent-tools-vanilla-opt-out │ │ │ │ │ └── index.ts │ │ │ │ ├── helpers │ │ │ │ │ ├── mock-cmp-wrapper.ts │ │ │ │ │ └── mock-cmp.ts │ │ │ │ └── onetrust │ │ │ │ │ └── index.ts │ │ │ ├── page-objects │ │ │ │ ├── base-page.ts │ │ │ │ ├── consent-tools-vanilla.ts │ │ │ │ └── onetrust.ts │ │ │ ├── tests │ │ │ │ ├── consent-tools-vanilla-opt-in.test.ts │ │ │ │ ├── consent-tools-vanilla-opt-out.test.ts │ │ │ │ └── onetrust.test.ts │ │ │ └── types │ │ │ │ └── analytics.d.ts │ │ ├── tsconfig.json │ │ └── webpack.config.ts │ ├── consent-tools │ │ ├── .eslintrc.js │ │ ├── .lintstagedrc.js │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── jest.config.js │ │ ├── jest.setup.js │ │ ├── package.json │ │ ├── src │ │ │ ├── domain │ │ │ │ ├── __tests__ │ │ │ │ │ ├── assertions │ │ │ │ │ │ └── integrations-assertions.ts │ │ │ │ │ ├── consent-stamping.test.ts │ │ │ │ │ ├── create-wrapper.test.ts │ │ │ │ │ ├── disable-segment.test.ts │ │ │ │ │ └── typedef-tests.ts │ │ │ │ ├── analytics │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ └── analytics-service.test.ts │ │ │ │ │ ├── analytics-service.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── blocking-helpers.ts │ │ │ │ ├── blocking-middleware.ts │ │ │ │ ├── config-helpers.ts │ │ │ │ ├── consent-stamping.ts │ │ │ │ ├── create-wrapper.ts │ │ │ │ ├── load-context.ts │ │ │ │ ├── logger.ts │ │ │ │ ├── pruned-categories.ts │ │ │ │ └── validation │ │ │ │ │ ├── __tests__ │ │ │ │ │ ├── options-validators.test.ts │ │ │ │ │ └── validation-error.test.ts │ │ │ │ │ ├── common-validators.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── options-validators.ts │ │ │ │ │ └── validation-error.ts │ │ │ ├── index.ts │ │ │ ├── test-helpers │ │ │ │ └── mocks │ │ │ │ │ ├── analytics-mock.ts │ │ │ │ │ └── index.ts │ │ │ ├── types │ │ │ │ ├── errors.ts │ │ │ │ ├── index.ts │ │ │ │ ├── settings.ts │ │ │ │ └── wrapper.ts │ │ │ └── utils │ │ │ │ ├── index.ts │ │ │ │ ├── pick.ts │ │ │ │ ├── pipe.ts │ │ │ │ ├── resolve-when.ts │ │ │ │ ├── ts-helpers.ts │ │ │ │ └── uniq.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ └── consent-wrapper-onetrust │ │ ├── .eslintrc.js │ │ ├── .lintstagedrc.js │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── bundle-build-tests │ │ ├── global.test.ts │ │ └── umd.test.ts │ │ ├── img │ │ ├── consent-mgmt-ui.png │ │ ├── onetrust-cat-id.jpg │ │ └── onetrust-popup.jpg │ │ ├── jest.config.js │ │ ├── jest.setup.js │ │ ├── package.json │ │ ├── src │ │ ├── domain │ │ │ ├── __tests__ │ │ │ │ └── wrapper.test.ts │ │ │ └── wrapper.ts │ │ ├── index.ts │ │ ├── index.umd.ts │ │ ├── lib │ │ │ ├── __tests__ │ │ │ │ └── onetrust-api.test.ts │ │ │ ├── onetrust-api.ts │ │ │ └── validation │ │ │ │ ├── index.ts │ │ │ │ └── onetrust-api-error.ts │ │ └── test-helpers │ │ │ ├── mocks.ts │ │ │ ├── onetrust-globals.d.ts │ │ │ └── utils.ts │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ └── webpack.config.js ├── core-integration-tests │ ├── .eslintrc.js │ ├── .lintstagedrc.js │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── public-api.test.ts │ │ └── typedef-tests.ts │ └── tsconfig.json ├── core │ ├── .eslintrc.js │ ├── .lintstagedrc.js │ ├── CHANGELOG.md │ ├── LICENSE.MD │ ├── README.md │ ├── jest.config.js │ ├── jest.setup.js │ ├── package.json │ ├── src │ │ ├── analytics │ │ │ ├── __tests__ │ │ │ │ └── dispatch.test.ts │ │ │ ├── dispatch.ts │ │ │ └── index.ts │ │ ├── callback │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ └── index.ts │ │ ├── context │ │ │ └── index.ts │ │ ├── emitter │ │ │ └── interface.ts │ │ ├── events │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ ├── index.ts │ │ │ └── interfaces.ts │ │ ├── index.ts │ │ ├── logger │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ └── index.ts │ │ ├── plugins │ │ │ └── index.ts │ │ ├── priority-queue │ │ │ ├── __tests__ │ │ │ │ ├── backoff.test.ts │ │ │ │ └── index.test.ts │ │ │ ├── backoff.ts │ │ │ └── index.ts │ │ ├── queue │ │ │ ├── __tests__ │ │ │ │ ├── event-queue.test.ts │ │ │ │ └── extension-flushing.test.ts │ │ │ ├── delivery.ts │ │ │ └── event-queue.ts │ │ ├── stats │ │ │ ├── __tests__ │ │ │ │ └── index.test.ts │ │ │ └── index.ts │ │ ├── task │ │ │ ├── __tests__ │ │ │ │ └── task-group.test.ts │ │ │ └── task-group.ts │ │ ├── user │ │ │ └── index.ts │ │ ├── utils │ │ │ ├── __tests__ │ │ │ │ ├── group-by.test.ts │ │ │ │ ├── is-plain-object.test.ts │ │ │ │ └── is-thenable.test.ts │ │ │ ├── bind-all.ts │ │ │ ├── get-global.ts │ │ │ ├── group-by.ts │ │ │ ├── has-properties.ts │ │ │ ├── is-plain-object.ts │ │ │ ├── is-thenable.ts │ │ │ ├── p-while.ts │ │ │ ├── pick.ts │ │ │ └── ts-helpers.ts │ │ └── validation │ │ │ ├── __tests__ │ │ │ └── assertions.test.ts │ │ │ ├── assertions.ts │ │ │ ├── errors.ts │ │ │ └── helpers.ts │ ├── test-helpers │ │ ├── index.ts │ │ ├── test-ctx.ts │ │ └── test-event-queue.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── generic-utils │ ├── .eslintrc.js │ ├── .lintstagedrc.js │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── create-deferred │ │ │ ├── __tests__ │ │ │ │ └── create-deferred.test.ts │ │ │ ├── create-deferred.ts │ │ │ └── index.ts │ │ ├── emitter │ │ │ ├── __tests__ │ │ │ │ └── emitter.test.ts │ │ │ ├── emitter.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── node-integration-tests │ ├── .eslintrc.js │ ├── .lintstagedrc.js │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── cloudflare-tests │ │ │ ├── index.test.ts │ │ │ └── workers │ │ │ │ ├── README.md │ │ │ │ ├── forgot-close-and-flush.ts │ │ │ │ ├── send-each-event-type.ts │ │ │ │ ├── send-multiple-events.ts │ │ │ │ ├── send-oauth-event.ts │ │ │ │ └── send-single-event.ts │ │ ├── common │ │ │ └── ddos.ts │ │ ├── durability-tests │ │ │ ├── durability-tests.ts │ │ │ └── server-start-analytics.ts │ │ ├── perf-tests │ │ │ ├── perf-tests.ts │ │ │ ├── server-start-analytics.ts │ │ │ ├── server-start-no-analytics.ts │ │ │ └── server-start-old-analytics.ts │ │ ├── server │ │ │ ├── autocannon.ts │ │ │ ├── fetch-polyfill.ts │ │ │ ├── fixtures.ts │ │ │ ├── mock-segment-workers.ts │ │ │ ├── nock.ts │ │ │ ├── server.ts │ │ │ └── types.ts │ │ └── smoke │ │ │ └── smoke.ts │ └── tsconfig.json ├── node │ ├── .eslintrc.js │ ├── .lintstagedrc.js │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── jest.setup.js │ ├── package.json │ ├── scripts │ │ └── version.sh │ ├── src │ │ ├── __tests__ │ │ │ ├── argument-validation.integration.test.ts │ │ │ ├── callback.test.ts │ │ │ ├── disable.integration.test.ts │ │ │ ├── emitter.integration.test.ts │ │ │ ├── graceful-shutdown-integration.test.ts │ │ │ ├── http-client.integration.test.ts │ │ │ ├── http-integration.test.ts │ │ │ ├── integration.test.ts │ │ │ ├── oauth.integration.test.ts │ │ │ ├── plugins.test.ts │ │ │ ├── settings.test.ts │ │ │ ├── test-helpers │ │ │ │ ├── assert-shape │ │ │ │ │ ├── http-request-event.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── segment-http-api.ts │ │ │ │ ├── create-test-analytics.ts │ │ │ │ ├── factories.ts │ │ │ │ ├── is-valid-date.ts │ │ │ │ ├── resolve-ctx.ts │ │ │ │ ├── resolve-emitter.ts │ │ │ │ ├── sleep.ts │ │ │ │ └── test-plugin.ts │ │ │ └── typedef-tests.ts │ │ ├── app │ │ │ ├── analytics-node.ts │ │ │ ├── context.ts │ │ │ ├── dispatch-emit.ts │ │ │ ├── emitter.ts │ │ │ ├── event-factory.ts │ │ │ ├── event-queue.ts │ │ │ ├── settings.ts │ │ │ └── types │ │ │ │ ├── index.ts │ │ │ │ ├── params.ts │ │ │ │ ├── plugin.ts │ │ │ │ └── segment-event.ts │ │ ├── generated │ │ │ └── version.ts │ │ ├── index.common.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── __tests__ │ │ │ │ ├── abort.test.ts │ │ │ │ ├── create-url.test.ts │ │ │ │ ├── env.test.ts │ │ │ │ ├── get-message-id.test.ts │ │ │ │ └── token-manager.test.ts │ │ │ ├── abort.ts │ │ │ ├── base-64-encode.ts │ │ │ ├── create-url.ts │ │ │ ├── env.ts │ │ │ ├── fetch.ts │ │ │ ├── get-message-id.ts │ │ │ ├── http-client.ts │ │ │ ├── token-manager.ts │ │ │ ├── types.ts │ │ │ └── uuid.ts │ │ └── plugins │ │ │ └── segmentio │ │ │ ├── __tests__ │ │ │ ├── methods.test.ts │ │ │ └── publisher.test.ts │ │ │ ├── context-batch.ts │ │ │ ├── index.ts │ │ │ └── publisher.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── page-tools │ ├── .eslintrc.js │ ├── .lintstagedrc.js │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── page-context.test.ts │ │ ├── index.ts │ │ └── page-context.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── tsup.config.js ├── signals │ ├── signals-example │ │ ├── .babelrc │ │ ├── .env.example │ │ ├── .eslintrc.js │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── package.json │ │ ├── public │ │ │ └── index.html │ │ ├── src │ │ │ ├── components │ │ │ │ ├── App.tsx │ │ │ │ ├── ComplexForm.tsx │ │ │ │ └── ComplexReactHookForm.tsx │ │ │ ├── index.tsx │ │ │ ├── lib │ │ │ │ └── analytics.ts │ │ │ ├── pages │ │ │ │ ├── Home.tsx │ │ │ │ ├── Other.tsx │ │ │ │ └── ReactHookForm.tsx │ │ │ └── styles.css │ │ ├── tsconfig.json │ │ └── webpack.config.js │ ├── signals-integration-tests │ │ ├── .babelrc │ │ ├── .eslintrc.js │ │ ├── .lintstagedrc.js │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── playwright.global-setup.ts │ │ ├── src │ │ │ ├── helpers │ │ │ │ ├── base-page-object.ts │ │ │ │ ├── env-config.ts │ │ │ │ ├── fixtures.ts │ │ │ │ ├── log-console.ts │ │ │ │ ├── network-utils.ts │ │ │ │ ├── playwright-utils.ts │ │ │ │ └── ts.ts │ │ │ ├── shims.d.ts │ │ │ └── tests │ │ │ │ ├── custom-elements │ │ │ │ ├── components │ │ │ │ │ ├── App.tsx │ │ │ │ │ ├── Button.css │ │ │ │ │ ├── Button.tsx │ │ │ │ │ ├── Checkbox.css │ │ │ │ │ ├── Checkbox.tsx │ │ │ │ │ ├── ComboBox.css │ │ │ │ │ ├── ComboBox.tsx │ │ │ │ │ ├── Dialog.css │ │ │ │ │ ├── Dialog.tsx │ │ │ │ │ ├── Form.css │ │ │ │ │ ├── Form.tsx │ │ │ │ │ ├── ListBox.css │ │ │ │ │ ├── ListBox.tsx │ │ │ │ │ ├── Modal.css │ │ │ │ │ ├── Modal.tsx │ │ │ │ │ ├── Popover.css │ │ │ │ │ ├── Popover.tsx │ │ │ │ │ ├── Select.css │ │ │ │ │ ├── Select.tsx │ │ │ │ │ ├── Switch.css │ │ │ │ │ ├── Switch.tsx │ │ │ │ │ ├── Tabs.css │ │ │ │ │ ├── Tabs.tsx │ │ │ │ │ ├── TextField.css │ │ │ │ │ ├── TextField.tsx │ │ │ │ │ └── theme.css │ │ │ │ ├── custom-select.test.ts │ │ │ │ ├── custom-textfield.test.ts │ │ │ │ ├── index-page.ts │ │ │ │ ├── index.bundle.tsx │ │ │ │ └── index.html │ │ │ │ ├── performance │ │ │ │ ├── index-page.ts │ │ │ │ ├── index.html │ │ │ │ └── memory-leak.test.ts │ │ │ │ └── signals-vanilla │ │ │ │ ├── all-segment-events.test.ts │ │ │ │ ├── basic.test.ts │ │ │ │ ├── button-click-complex.test.ts │ │ │ │ ├── change-input.test.ts │ │ │ │ ├── index-page.ts │ │ │ │ ├── index.bundle.ts │ │ │ │ ├── index.html │ │ │ │ ├── middleware.test.ts │ │ │ │ ├── network-signals-allow-list.test.ts │ │ │ │ ├── network-signals-fetch.test.ts │ │ │ │ ├── network-signals-xhr.test.ts │ │ │ │ ├── reset.test.ts │ │ │ │ ├── runtime-constants.test.ts │ │ │ │ ├── signals-find.test.ts │ │ │ │ ├── signals-ingestion.test.ts │ │ │ │ ├── signals-redaction.test.ts │ │ │ │ ├── snapshots │ │ │ │ └── all-segment-events-snapshot.json │ │ │ │ └── top-level-metadata.test.ts │ │ ├── tsconfig.json │ │ └── webpack.config.ts │ ├── signals-runtime │ │ ├── .eslintrc.js │ │ ├── .lintstagedrc.js │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── api-extractor.base.json │ │ ├── api-extractor.mobile.json │ │ ├── api-extractor.web.json │ │ ├── build-editor-types.js │ │ ├── build-signals-runtime-global.js │ │ ├── bundle-build-tests │ │ │ └── runtime-env.test.ts │ │ ├── jest.config.js │ │ ├── jest.setup.js │ │ ├── package.json │ │ ├── scripts │ │ │ └── assert-generated.sh │ │ ├── src │ │ │ ├── __tests__ │ │ │ │ └── signals-runtime.test.ts │ │ │ ├── index.ts │ │ │ ├── mobile │ │ │ │ ├── get-runtime-code.generated.ts │ │ │ │ ├── index.mobile-editor.ts │ │ │ │ ├── index.signals-runtime.ts │ │ │ │ ├── mobile-constants.ts │ │ │ │ ├── mobile-signals-runtime.ts │ │ │ │ └── mobile-signals-types.ts │ │ │ ├── shared │ │ │ │ ├── shared-types.ts │ │ │ │ └── signals-runtime.ts │ │ │ ├── test-helpers │ │ │ │ └── mocks │ │ │ │ │ └── mock-signal-types-web.ts │ │ │ └── web │ │ │ │ ├── get-runtime-code.generated.ts │ │ │ │ ├── index.signals-runtime.ts │ │ │ │ ├── index.web-editor.ts │ │ │ │ ├── web-constants.ts │ │ │ │ ├── web-signals-runtime.ts │ │ │ │ └── web-signals-types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ └── signals │ │ ├── .eslintrc.js │ │ ├── .lintstagedrc.js │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── jest.config.js │ │ ├── jest.setup.ts │ │ ├── package.json │ │ ├── scripts │ │ ├── assert-workerbox-built.sh │ │ ├── build-workerbox.js │ │ └── version.sh │ │ ├── src │ │ ├── core │ │ │ ├── analytics-service │ │ │ │ ├── __tests__ │ │ │ │ │ └── analytics-service.test.ts │ │ │ │ └── index.ts │ │ │ ├── buffer │ │ │ │ ├── __tests__ │ │ │ │ │ └── buffer.test.ts │ │ │ │ └── index.ts │ │ │ ├── debug-mode │ │ │ │ └── index.ts │ │ │ ├── emitter │ │ │ │ ├── __tests__ │ │ │ │ │ └── signal-emitter.test.ts │ │ │ │ └── index.ts │ │ │ ├── middleware │ │ │ │ ├── event-processor │ │ │ │ │ └── index.ts │ │ │ │ ├── network-signals-filter │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ └── network-signals-filter.test.ts │ │ │ │ │ └── network-signals-filter.ts │ │ │ │ ├── signals-ingest │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── client.test.ts │ │ │ │ │ │ └── redact.test.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── redact.ts │ │ │ │ │ └── signals-ingest-client.ts │ │ │ │ └── user-info │ │ │ │ │ └── index.ts │ │ │ ├── processor │ │ │ │ ├── __tests__ │ │ │ │ │ └── sandbox-settings.test.ts │ │ │ │ ├── arg-resolvers.ts │ │ │ │ ├── polyfills.ts │ │ │ │ ├── processor.ts │ │ │ │ └── sandbox.ts │ │ │ ├── signal-generators │ │ │ │ ├── dom-gen │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── change-gen.test.ts │ │ │ │ │ │ ├── clean-text.test.ts │ │ │ │ │ │ ├── element-parser.test.ts │ │ │ │ │ │ ├── mutation-observer.test.ts │ │ │ │ │ │ └── navigation-gen.test.ts │ │ │ │ │ ├── change-gen.ts │ │ │ │ │ ├── dom-gen.ts │ │ │ │ │ ├── element-parser.ts │ │ │ │ │ ├── helpers.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── mutation-observer.ts │ │ │ │ │ └── navigation-gen.ts │ │ │ │ ├── network-gen │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── helpers.test.ts │ │ │ │ │ │ ├── network-generator.test.ts │ │ │ │ │ │ └── network-interceptor.test.ts │ │ │ │ │ ├── helpers.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── network-interceptor.ts │ │ │ │ ├── register.ts │ │ │ │ └── types.ts │ │ │ └── signals │ │ │ │ ├── index.ts │ │ │ │ ├── settings.ts │ │ │ │ └── signals.ts │ │ ├── generated │ │ │ └── version.ts │ │ ├── index.ts │ │ ├── index.umd.ts │ │ ├── lib │ │ │ ├── assert-browser-env │ │ │ │ └── index.ts │ │ │ ├── debounce │ │ │ │ ├── __tests__ │ │ │ │ │ └── debounce.test.ts │ │ │ │ └── index.ts │ │ │ ├── detect-url-change │ │ │ │ └── index.ts │ │ │ ├── exists │ │ │ │ └── index.ts │ │ │ ├── load-script │ │ │ │ └── index.ts │ │ │ ├── logger │ │ │ │ └── index.ts │ │ │ ├── normalize-url │ │ │ │ ├── __tests__ │ │ │ │ │ └── normalize-url.test.ts │ │ │ │ └── index.ts │ │ │ ├── page-data │ │ │ │ └── index.ts │ │ │ ├── replace-base-url │ │ │ │ ├── __tests__ │ │ │ │ │ └── replace-base-url.test.ts │ │ │ │ └── index.ts │ │ │ ├── storage │ │ │ │ ├── __tests__ │ │ │ │ │ └── web-storage.test.ts │ │ │ │ └── web-storage.ts │ │ │ └── workerbox │ │ │ │ ├── __mocks__ │ │ │ │ └── workerbox.ts │ │ │ │ ├── index.ts │ │ │ │ ├── worker.generated.ts │ │ │ │ ├── worker.html │ │ │ │ └── worker.ts │ │ ├── plugin │ │ │ ├── __tests__ │ │ │ │ └── signals-plugin.test.ts │ │ │ └── signals-plugin.ts │ │ ├── test-helpers │ │ │ ├── jest-extended.ts │ │ │ ├── mocks │ │ │ │ ├── analytics-mock.ts │ │ │ │ ├── factories.ts │ │ │ │ └── index.ts │ │ │ ├── range.ts │ │ │ └── set-location.ts │ │ ├── types │ │ │ ├── __tests__ │ │ │ │ └── create-network-signal.test.ts │ │ │ ├── analytics-api.ts │ │ │ ├── factories.ts │ │ │ ├── index.ts │ │ │ ├── process-signal.ts │ │ │ └── settings.ts │ │ └── utils │ │ │ ├── index.ts │ │ │ ├── is-class.ts │ │ │ └── ts-helpers.ts │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ └── webpack.config.js └── test-helpers │ ├── .eslintrc.js │ ├── .lintstagedrc.js │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── analytics │ │ ├── cdn-settings-builder.ts │ │ └── index.ts │ ├── constants │ │ └── index.ts │ ├── index.ts │ ├── jest │ │ └── serializers │ │ │ ├── index.ts │ │ │ └── timestamp.ts │ └── utils │ │ ├── index.ts │ │ ├── promise-timeout.ts │ │ └── sleep.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── playgrounds ├── README.md ├── next-playground │ ├── .eslintrc.js │ ├── .gitignore │ ├── .lintstagedrc.js │ ├── README.md │ ├── context │ │ └── analytics.tsx │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── iframe │ │ │ ├── childPage.tsx │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── partytown │ │ │ └── index.tsx │ │ └── vanilla │ │ │ ├── index.tsx │ │ │ └── other-page.tsx │ ├── public │ │ ├── demos │ │ │ ├── faulty-load.js │ │ │ ├── faulty-middleware.js │ │ │ ├── faulty-track.js │ │ │ ├── identity.js │ │ │ ├── signals.js │ │ │ └── tracking-plan.js │ │ └── fira-code.webfont │ │ │ ├── fira-code_regular.eot │ │ │ ├── fira-code_regular.svg │ │ │ ├── fira-code_regular.ttf │ │ │ ├── fira-code_regular.woff │ │ │ └── webfont.css │ ├── styles │ │ ├── dracula │ │ │ ├── avatar.css │ │ │ ├── badge.css │ │ │ ├── button.css │ │ │ ├── card.css │ │ │ ├── colors.css │ │ │ ├── dracula-ui.css │ │ │ ├── input.css │ │ │ ├── prism.css │ │ │ ├── radio-checkbox-switch.css │ │ │ ├── select.css │ │ │ ├── sizes.css │ │ │ └── typography.css │ │ ├── globals.css │ │ └── logs-table.css │ ├── tsconfig.json │ └── utils │ │ └── hooks │ │ ├── useConfig.ts │ │ ├── useDidMountEffect.ts │ │ └── useLocalStorage.ts ├── standalone-playground │ ├── README.md │ ├── index.html │ ├── package.json │ └── pages │ │ ├── index-buffered-page-ctx.html │ │ ├── index-consent-opt-in.html │ │ ├── index-consent-opt-out.html │ │ ├── index-local-batched.html │ │ ├── index-local-csp.html │ │ ├── index-local-errors.html │ │ ├── index-local-track-link.html │ │ ├── index-local.html │ │ ├── index-remote.html │ │ └── index-signals.html └── with-vite │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ ├── App.css │ ├── App.tsx │ ├── favicon.svg │ ├── index.css │ ├── logo.svg │ ├── main.tsx │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── scripts ├── .eslintrc.js ├── .lintstagedrc.js ├── clean.sh ├── create-release-from-tags │ ├── __tests__ │ │ ├── fixtures │ │ │ ├── first-release-example.md │ │ │ └── reg-example.md │ │ └── index.test.ts │ ├── index.ts │ └── run.ts ├── env.d.ts ├── jest.config.js ├── package.json ├── purge-cdn-cache.js ├── tsconfig.json ├── update-lockfile.sh └── utils │ └── exists.ts ├── tsconfig.json ├── turbo.json ├── typings ├── get-monorepo-packages.d.ts └── spawn.d.ts └── yarn.lock /.buildkite/Dockerfile.agent: -------------------------------------------------------------------------------- 1 | FROM 528451384384.dkr.ecr.us-west-2.amazonaws.com/buildkite-agent-node20 2 | 3 | RUN npx playwright install-deps 4 | 5 | ENTRYPOINT [] 6 | -------------------------------------------------------------------------------- /.buildkite/Makefile: -------------------------------------------------------------------------------- 1 | ECR_REPOSITORY = 528451384384.dkr.ecr.us-west-2.amazonaws.com 2 | IMAGE = ${ECR_REPOSITORY}/analytics-next-ci-agent:latest 3 | 4 | agent: 5 | docker build --pull . -f Dockerfile.agent -t ${IMAGE} 6 | aws-okta exec ops-write -- docker push ${IMAGE} 7 | .PHONY: agent -------------------------------------------------------------------------------- /.buildkite/Readme.md: -------------------------------------------------------------------------------- 1 | # Buildkite 2 | 3 | ## How to update the buildkite docker agent 4 | 1. Make your changes to `Dockerfile.agent`. 5 | 2. Push the changes to ecr 6 | (will need `Ops Write` permission). 7 | ```bash 8 | $ cd .buildkite 9 | $ robo-tooling.docker.login-privileged 10 | $ make agent 11 | ``` 12 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json", 3 | "ignore": ["@playground/*"], 4 | "changelog": [ 5 | "@changesets/changelog-github", 6 | { 7 | "repo": "segmentio/analytics-next" 8 | } 9 | ], 10 | "commit": false, 11 | "access": "public", 12 | "baseBranch": "master", 13 | "linked": [], 14 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 15 | "onlyUpdatePeerDependentsWhenOutOfRange": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc.isomorphic.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['./.eslintrc'], 4 | plugins: ['import'], 5 | overrides: [ 6 | { 7 | // this library should not have any node OR browser runtime dependencies. 8 | // In particular, nextjs and vercel edge functions will break if _any_ node dependencies are introduced. 9 | files: ['src/**'], 10 | excludedFiles: ['**/__tests__/**'], 11 | rules: { 12 | 'no-restricted-globals': [ 13 | 'error', 14 | 'document', 15 | 'window', 16 | 'self', 17 | 'process', 18 | 'global', 19 | 'navigator', 20 | 'location', 21 | ], 22 | 'import/no-nodejs-modules': 'error', 23 | }, 24 | }, 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @libraries-web-team 2 | @nettofarah 3 | @juliofarah 4 | @danieljackins 5 | @pooyaj 6 | @dk1027 -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | 10 | 11 | - [ ] I've included a changeset (psst. run `yarn changeset`. Read about changesets [here](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md)). 12 | -------------------------------------------------------------------------------- /.github/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-next/c0d58e3fc12e9733eacfac7619d7369a2e297d35/.github/architecture.png -------------------------------------------------------------------------------- /.github/workflows/md-link-check.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": [ 3 | { 4 | "pattern": "^http://127\.0\.0\.1.*" 5 | }, 6 | { 7 | "pattern": "^http://localhost.*" 8 | } 9 | ], 10 | "replacementPatterns": [ 11 | { 12 | "pattern": "^/", 13 | "replacement": "{{BASEURL}}/" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/md-link-check.yml: -------------------------------------------------------------------------------- 1 | name: Markdown Links Check 2 | on: 3 | schedule: 4 | # Runs once every 3 days 5 | - cron: "0 0 */3 * *" 6 | jobs: 7 | check-links: 8 | name: Check Markdown Links 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - uses: gaurav-nelson/github-action-markdown-link-check@v1 13 | with: 14 | use-quiet-mode: "yes" 15 | use-verbose-mode: "yes" 16 | config-file: ".github/workflows/md-link-check.config.json" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # yarn artifacts - https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 2 | .pnp.* 3 | .yarn/* 4 | !.yarn/patches 5 | !.yarn/plugins 6 | !.yarn/releases 7 | !.yarn/sdks 8 | !.yarn/versions 9 | 10 | # ignore all .vscode folders unless they're in the root directory 11 | .vscode 12 | !.vscode/extensions.json 13 | !.vscode/launch.json 14 | !.vscode/settings.json 15 | 16 | node_modules 17 | dist 18 | dist.* 19 | package-lock.json 20 | .DS_Store 21 | *.log 22 | stats.json 23 | .tmp 24 | *.tsbuildinfo 25 | coverage 26 | reports/* 27 | 28 | # ignore archives 29 | *.tgz 30 | *.gz 31 | 32 | .changelog 33 | .turbo 34 | test-results/ 35 | playwright-report/ 36 | playwright/.cache/ 37 | tmp.tsconfig.json 38 | .env 39 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn prepush 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.9.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "tabWidth": 2, 6 | "arrowParens": "always" 7 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "wix.vscode-import-cost"] 3 | } 4 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 5 | spec: "@yarnpkg/plugin-workspace-tools" 6 | - path: .yarn/plugins/@yarnpkg/plugin-constraints.cjs 7 | spec: "@yarnpkg/plugin-constraints" 8 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 9 | spec: "@yarnpkg/plugin-interactive-tools" 10 | - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs 11 | spec: "@yarnpkg/plugin-typescript" 12 | 13 | preferInteractive: true 14 | 15 | yarnPath: .yarn/releases/yarn-3.4.1.cjs 16 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 0% 7 | base: auto 8 | comment: 9 | show_carryforward_flags: true 10 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-next/c0d58e3fc12e9733eacfac7619d7369a2e297d35/example.png -------------------------------------------------------------------------------- /img/twilio-segment-logo-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-next/c0d58e3fc12e9733eacfac7619d7369a2e297d35/img/twilio-segment-logo-2x.png -------------------------------------------------------------------------------- /packages/browser-integration-tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../.eslintrc'], 4 | } 5 | -------------------------------------------------------------------------------- /packages/browser-integration-tests/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/browser-integration-tests/README.md: -------------------------------------------------------------------------------- 1 | Core tests that require AnalyticsBrowser, etc. 2 | This exists because we can't create circular dependencies -- so, for example, installing AnalyticsBrowser as a dev dependency on core. -------------------------------------------------------------------------------- /packages/browser-integration-tests/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname) 4 | -------------------------------------------------------------------------------- /packages/browser-integration-tests/src/helpers/extract-writekey.ts: -------------------------------------------------------------------------------- 1 | export function extractWriteKeyFromUrl(url: string): string | undefined { 2 | const matches = url.match( 3 | /https:\/\/cdn.segment.com\/v1\/projects\/(.+)\/settings/ 4 | ) 5 | 6 | if (matches) { 7 | return matches[1] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/browser-integration-tests/src/shims.d.ts: -------------------------------------------------------------------------------- 1 | import type { AnalyticsSnippet } from '@segment/analytics-next' 2 | 3 | declare global { 4 | interface Window { 5 | analytics: AnalyticsSnippet 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/browser-integration-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "exclude": ["node_modules", "dist"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "target": "ES5", 7 | "moduleResolution": "node", 8 | "lib": ["es2020"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/browser/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | ignorePatterns: ['e2e-tests', 'qa', '/*.tmp.*/'], 4 | extends: ['../../.eslintrc'], 5 | env: { 6 | node: true, // TODO: change to false when node is abstracted out 7 | browser: true, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /packages/browser/.gitignore: -------------------------------------------------------------------------------- 1 | umd.old 2 | -------------------------------------------------------------------------------- /packages/browser/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/browser/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname, { 4 | modulePathIgnorePatterns: ['/e2e-tests', '/qa'], 5 | setupFilesAfterEnv: ['./jest.setup.js'], 6 | testEnvironment: 'jsdom', 7 | coverageThreshold: { 8 | global: { 9 | branches: 0, 10 | functions: 0, 11 | lines: 0, 12 | statements: 0, 13 | }, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /packages/browser/jest.setup.js: -------------------------------------------------------------------------------- 1 | const { TextEncoder, TextDecoder } = require('util') 2 | const { setImmediate } = require('timers') 3 | 4 | // fix: "ReferenceError: TextEncoder is not defined" after upgrading JSDOM 5 | global.TextEncoder = TextEncoder 6 | global.TextDecoder = TextDecoder 7 | // fix: jsdom uses setImmediate under the hood for preflight XHR requests, 8 | // and jest removed setImmediate, so we need to provide it to prevent console 9 | // logging ReferenceErrors made by integration tests that call Amplitude. 10 | global.setImmediate = setImmediate 11 | -------------------------------------------------------------------------------- /packages/browser/qa/lib/browser.ts: -------------------------------------------------------------------------------- 1 | import playwright, { Browser } from 'playwright' 2 | 3 | const debug = process.env.DEBUG ?? false 4 | 5 | let br: Browser 6 | export async function browser(): Promise { 7 | if (!br) { 8 | br = await playwright.chromium.launch({ 9 | devtools: true, 10 | headless: !debug, 11 | args: [ 12 | '--no-sandbox', 13 | '--disable-setuid-sandbox', 14 | '--disable-dev-shm-usage', 15 | '--disable-accelerated-2d-canvas', 16 | '--no-zygote', 17 | '--disable-gpu', 18 | ], 19 | }) 20 | } 21 | 22 | return br 23 | } 24 | 25 | !debug && 26 | process.on('exit', async () => { 27 | await br?.close() 28 | }) 29 | -------------------------------------------------------------------------------- /packages/browser/scripts/build-prep.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PKG_VERSION=$(node --eval="process.stdout.write(require('./package.json').version)") 3 | 4 | cat <src/generated/version.ts 5 | // This file is generated. 6 | export const version = '$PKG_VERSION' 7 | EOF 8 | -------------------------------------------------------------------------------- /packages/browser/scripts/ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo '--- Build bundles' 5 | make build 6 | 7 | echo '--- Check Size' 8 | yarn run -T browser size-limit 9 | 10 | echo '--- Lint files' 11 | make lint 12 | 13 | echo '--- Run tests' 14 | make test-unit 15 | make test-integration 16 | -------------------------------------------------------------------------------- /packages/browser/scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | : "${SEGMENT_LIB_PATH:?"is a required environment variable"}" 6 | : "${AJS_PRIVATE_ASSETS_UPLOAD:?"is a required environment variable"}" 7 | 8 | source "${SEGMENT_LIB_PATH}/aws.bash" 9 | 10 | function main() { 11 | node scripts/release.js 12 | } 13 | 14 | run-with-role ${AJS_PRIVATE_ASSETS_UPLOAD} main 15 | -------------------------------------------------------------------------------- /packages/browser/scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Allow the service to run without chamber (for ci, docker-compose, etc) 4 | if [ -z "$NO_CHAMBER" ];then 5 | exec chamber exec analytics-next -- node dist/src/boot.js 6 | else 7 | exec node dist/src/boot.js 8 | fi; 9 | -------------------------------------------------------------------------------- /packages/browser/scripts/vendor/README.md: -------------------------------------------------------------------------------- 1 | # Vendor library 2 | 3 | This script vendors the following library: 4 | https://github.com/segmentio/tsub-js 5 | 6 | Usage for updating tsub: 7 | - update tsub to new version (tsub should be a _dev dependency_) 8 | - run `yarn vendor` from package root -------------------------------------------------------------------------------- /packages/browser/scripts/vendor/run.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file: 3 | * - uses webpack to build tsub.js 4 | * - converts tsub.js to tsub.ts, appends comments, and moves it into the source directory 5 | */ 6 | const { execSync } = require('node:child_process') 7 | const path = require('node:path') 8 | const { createTSFromJSLib } = require('./helpers') 9 | 10 | // build tsub.js with webpack 11 | const configPath = path.join(__dirname, 'webpack.config.vendor.js') 12 | execSync(`yarn webpack --config ${configPath}`, { stdio: 'inherit' }) 13 | 14 | // create tsub.ts artifact and move to source directory 15 | const tsubInputBundlePath = path.join(__dirname, 'dist.vendor', 'tsub.js') 16 | const tsubOutputVendorDir = 'src/vendor/tsub' 17 | createTSFromJSLib(tsubInputBundlePath, tsubOutputVendorDir, { 18 | libraryName: '@segment/tsub', 19 | }) 20 | -------------------------------------------------------------------------------- /packages/browser/scripts/vendor/webpack.config.vendor.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path') 2 | 3 | /** @type { import('webpack').Configuration } */ 4 | module.exports = { 5 | entry: require.resolve('@segment/tsub'), 6 | output: { 7 | path: path.resolve(__dirname, 'dist.vendor'), // Output directory 8 | filename: 'tsub.js', 9 | library: { 10 | type: 'umd', 11 | }, 12 | }, 13 | resolve: { 14 | extensions: ['.js'], // Resolve these extensions 15 | }, 16 | mode: 'production', // Use production mode for minification, etc. 17 | } 18 | -------------------------------------------------------------------------------- /packages/browser/src/browser/browser-umd.ts: -------------------------------------------------------------------------------- 1 | import { getCDN, setGlobalCDNUrl } from '../lib/parse-cdn' 2 | import { setVersionType } from '../lib/version-type' 3 | 4 | if (process.env.IS_WEBPACK_BUILD) { 5 | if (process.env.ASSET_PATH) { 6 | // @ts-ignore 7 | __webpack_public_path__ = process.env.ASSET_PATH 8 | } else { 9 | const cdn = getCDN() 10 | setGlobalCDNUrl(cdn) 11 | 12 | // @ts-ignore 13 | __webpack_public_path__ = cdn + '/analytics-next/bundles/' 14 | } 15 | } 16 | 17 | setVersionType('web') 18 | 19 | export * from '.' 20 | -------------------------------------------------------------------------------- /packages/browser/src/browser/standalone-interface.ts: -------------------------------------------------------------------------------- 1 | import { Analytics, InitOptions } from '../core/analytics' 2 | 3 | export interface AnalyticsSnippet extends AnalyticsStandalone { 4 | load: (writeKey: string, options?: InitOptions) => void 5 | } 6 | 7 | export interface AnalyticsStandalone extends Analytics { 8 | _loadOptions?: InitOptions 9 | _writeKey?: string 10 | _cdn?: string 11 | } 12 | -------------------------------------------------------------------------------- /packages/browser/src/core/callback/index.ts: -------------------------------------------------------------------------------- 1 | export { invokeCallback, pTimeout } from '@segment/analytics-core' 2 | -------------------------------------------------------------------------------- /packages/browser/src/core/connection/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { isOffline, isOnline } from '..' 2 | 3 | describe('connection', () => { 4 | let online = true 5 | 6 | Object.defineProperty(window.navigator, 'onLine', { 7 | get() { 8 | return online 9 | }, 10 | }) 11 | 12 | test('checks that the browser is online', () => { 13 | online = true 14 | 15 | expect(isOnline()).toBe(true) 16 | expect(isOffline()).toBe(false) 17 | }) 18 | 19 | test('checks that the browser is offline', () => { 20 | online = false 21 | 22 | expect(isOnline()).toBe(false) 23 | expect(isOffline()).toBe(true) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/browser/src/core/connection/index.ts: -------------------------------------------------------------------------------- 1 | import { isBrowser } from '../environment' 2 | 3 | export function isOnline(): boolean { 4 | if (isBrowser()) { 5 | return window.navigator.onLine 6 | } 7 | 8 | return true 9 | } 10 | 11 | export function isOffline(): boolean { 12 | return !isOnline() 13 | } 14 | -------------------------------------------------------------------------------- /packages/browser/src/core/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const SEGMENT_API_HOST = 'api.segment.io/v1' 2 | -------------------------------------------------------------------------------- /packages/browser/src/core/context/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CoreContext, 3 | ContextCancelation, 4 | ContextFailedDelivery, 5 | SerializedContext, 6 | CancelationOptions, 7 | } from '@segment/analytics-core' 8 | import { SegmentEvent } from '../events/interfaces' 9 | import { Stats } from '../stats' 10 | 11 | export class Context extends CoreContext { 12 | static override system() { 13 | return new this({ type: 'track', event: 'system' }) 14 | } 15 | constructor(event: SegmentEvent, id?: string) { 16 | super(event, id, new Stats()) 17 | } 18 | } 19 | 20 | export { ContextCancelation } 21 | export type { ContextFailedDelivery, SerializedContext, CancelationOptions } 22 | -------------------------------------------------------------------------------- /packages/browser/src/core/environment/index.ts: -------------------------------------------------------------------------------- 1 | export function isBrowser(): boolean { 2 | return typeof window !== 'undefined' 3 | } 4 | 5 | export function isServer(): boolean { 6 | return !isBrowser() 7 | } 8 | -------------------------------------------------------------------------------- /packages/browser/src/core/events/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CoreOptions, 3 | CoreSegmentEvent, 4 | Callback, 5 | Plan, 6 | TrackPlan, 7 | PlanEvent, 8 | JSONArray, 9 | JSONValue, 10 | JSONPrimitive, 11 | JSONObject, 12 | GroupTraits, 13 | UserTraits, 14 | Traits, 15 | } from '@segment/analytics-core' 16 | 17 | export interface Options extends CoreOptions {} 18 | 19 | export type { GroupTraits, UserTraits, Traits } 20 | 21 | export type EventProperties = Record 22 | 23 | export interface SegmentEvent extends CoreSegmentEvent {} 24 | 25 | export type { 26 | Plan, 27 | TrackPlan, 28 | PlanEvent, 29 | Callback, 30 | JSONArray, 31 | JSONValue, 32 | JSONPrimitive, 33 | JSONObject, 34 | } 35 | -------------------------------------------------------------------------------- /packages/browser/src/core/inspector/index.ts: -------------------------------------------------------------------------------- 1 | import { getGlobal } from '../../lib/get-global' 2 | import type { Analytics } from '../analytics' 3 | 4 | const env = getGlobal() 5 | 6 | // The code below assumes the inspector extension will use Object.assign 7 | // to add the inspect interface on to this object reference (unless the 8 | // extension code ran first and has already set up the variable) 9 | const inspectorHost: { 10 | attach: (analytics: Analytics) => void 11 | } = ((env as any)['__SEGMENT_INSPECTOR__'] ??= {}) 12 | 13 | export const attachInspector = (analytics: Analytics) => 14 | inspectorHost.attach?.(analytics as any) 15 | -------------------------------------------------------------------------------- /packages/browser/src/core/page/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | BufferedPageContextDiscriminant, 3 | createBufferedPageContext, 4 | createPageContext, 5 | getDefaultBufferedPageContext, 6 | getDefaultPageContext, 7 | isBufferedPageContext, 8 | type PageContext, 9 | type BufferedPageContext, 10 | } from '@segment/analytics-page-tools' 11 | 12 | export * from './add-page-context' 13 | -------------------------------------------------------------------------------- /packages/browser/src/core/query-string/__tests__/gracefulDecodeURIComponent.test.ts: -------------------------------------------------------------------------------- 1 | import { gracefulDecodeURIComponent } from '../gracefulDecodeURIComponent' 2 | 3 | describe('gracefulDecodeURIComponent', () => { 4 | it('decodes a properly encoded URI component', () => { 5 | const output = gracefulDecodeURIComponent( 6 | 'brown+fox+jumped+%40+the+fence%3F' 7 | ) 8 | 9 | expect(output).toEqual('brown fox jumped @ the fence?') 10 | }) 11 | 12 | it('returns the input string back as-is when input is malformed', () => { 13 | const output = gracefulDecodeURIComponent('25%%2F35%') 14 | 15 | expect(output).toEqual('25%%2F35%') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/browser/src/core/query-string/__tests__/pickPrefix.test.ts: -------------------------------------------------------------------------------- 1 | import { pickPrefix } from '../pickPrefix' 2 | 3 | describe('pickPrefix', () => { 4 | it('strips a single prefix to creates an object', () => { 5 | const traits = pickPrefix('ajs_traits_', { 6 | ajs_traits_address: '12-123 St.', 7 | }) 8 | 9 | const output = { 10 | address: '12-123 St.', 11 | } 12 | 13 | expect(traits).toEqual(output) 14 | }) 15 | 16 | it('stripts multiple prefixes to create an object', () => { 17 | const traits = pickPrefix('ajs_traits_', { 18 | ajs_traits_address: '12-123 St.', 19 | ajs_traits_city: 'Vancouver', 20 | ajs_traits_province: 'BC', 21 | }) 22 | 23 | const output = { 24 | address: '12-123 St.', 25 | city: 'Vancouver', 26 | province: 'BC', 27 | } 28 | 29 | expect(traits).toEqual(output) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /packages/browser/src/core/query-string/gracefulDecodeURIComponent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tries to gets the unencoded version of an encoded component of a 3 | * Uniform Resource Identifier (URI). If input string is malformed, 4 | * returns it back as-is. 5 | * 6 | * Note: All occurences of the `+` character become ` ` (spaces). 7 | **/ 8 | export function gracefulDecodeURIComponent( 9 | encodedURIComponent: string 10 | ): string { 11 | try { 12 | return decodeURIComponent(encodedURIComponent.replace(/\+/g, ' ')) 13 | } catch { 14 | return encodedURIComponent 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/browser/src/core/query-string/pickPrefix.ts: -------------------------------------------------------------------------------- 1 | import { QueryStringParams } from '.' 2 | 3 | /** 4 | * Returns an object containing only the properties prefixed by the input 5 | * string. 6 | * Ex: prefix('ajs_traits_', { ajs_traits_address: '123 St' }) 7 | * will return { address: '123 St' } 8 | **/ 9 | export function pickPrefix( 10 | prefix: string, 11 | object: QueryStringParams 12 | ): QueryStringParams { 13 | return Object.keys(object).reduce((acc: QueryStringParams, key: string) => { 14 | if (key.startsWith(prefix)) { 15 | const field = key.substr(prefix.length) 16 | acc[field] = object[key]! 17 | } 18 | return acc 19 | }, {}) 20 | } 21 | -------------------------------------------------------------------------------- /packages/browser/src/core/queue/event-queue.ts: -------------------------------------------------------------------------------- 1 | import { PriorityQueue } from '../../lib/priority-queue' 2 | import { PersistedPriorityQueue } from '../../lib/priority-queue/persisted' 3 | import { Context } from '../context' 4 | import { Plugin } from '../plugin' 5 | import { CoreEventQueue } from '@segment/analytics-core' 6 | import { isOffline } from '../connection' 7 | 8 | export class EventQueue extends CoreEventQueue { 9 | constructor(name: string) 10 | constructor(priorityQueue: PriorityQueue) 11 | constructor(nameOrQueue: string | PriorityQueue) { 12 | super( 13 | typeof nameOrQueue === 'string' 14 | ? new PersistedPriorityQueue(4, nameOrQueue) 15 | : nameOrQueue 16 | ) 17 | } 18 | async flush(): Promise { 19 | if (isOffline()) return [] 20 | return super.flush() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/browser/src/core/stats/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { RemoteMetrics } from '../remote-metrics' 2 | import { Stats } from '..' 3 | 4 | const spy = jest.spyOn(RemoteMetrics.prototype, 'increment') 5 | 6 | describe(Stats, () => { 7 | test('forwards increments to remote metrics endpoint', () => { 8 | Stats.initRemoteMetrics() 9 | 10 | const stats = new Stats() 11 | stats.increment('banana', 1, ['phone:1']) 12 | 13 | expect(spy).toHaveBeenCalledWith('banana', ['phone:1']) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/browser/src/core/stats/index.ts: -------------------------------------------------------------------------------- 1 | import { CoreStats } from '@segment/analytics-core' 2 | import { MetricsOptions, RemoteMetrics } from './remote-metrics' 3 | 4 | let remoteMetrics: RemoteMetrics | undefined 5 | 6 | export class Stats extends CoreStats { 7 | static initRemoteMetrics(options?: MetricsOptions) { 8 | remoteMetrics = new RemoteMetrics(options) 9 | } 10 | 11 | override increment(metric: string, by?: number, tags?: string[]): void { 12 | super.increment(metric, by, tags) 13 | remoteMetrics?.increment(metric, tags ?? []) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/browser/src/core/stats/metric-helpers.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../context' 2 | 3 | export interface RecordIntegrationMetricProps { 4 | integrationName: string 5 | methodName: string 6 | didError?: boolean 7 | type: 'classic' | 'action' 8 | } 9 | 10 | export function recordIntegrationMetric( 11 | ctx: Context, 12 | { 13 | methodName, 14 | integrationName, 15 | type, 16 | didError = false, 17 | }: RecordIntegrationMetricProps 18 | ): void { 19 | ctx.stats.increment( 20 | `analytics_js.integration.invoke${didError ? '.error' : ''}`, 21 | 1, 22 | [ 23 | `method:${methodName}`, 24 | `integration_name:${integrationName}`, 25 | `type:${type}`, 26 | ] 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/browser/src/core/storage/__tests__/test-helpers.ts: -------------------------------------------------------------------------------- 1 | import jar from 'js-cookie' 2 | const throwDisabledError = () => { 3 | throw new Error('__sorry brah, this storage is disabled__') 4 | } 5 | /** 6 | * Disables Cookies 7 | * @returns jest spy 8 | */ 9 | export function disableCookies(): void { 10 | jest.spyOn(window.navigator, 'cookieEnabled', 'get').mockReturnValue(false) 11 | jest.spyOn(jar, 'set').mockImplementation(throwDisabledError) 12 | jest.spyOn(jar, 'get').mockImplementation(throwDisabledError) 13 | } 14 | 15 | /** 16 | * Disables LocalStorage 17 | * @returns jest spy 18 | */ 19 | export function disableLocalStorage(): void { 20 | jest 21 | .spyOn(Storage.prototype, 'setItem') 22 | .mockImplementation(throwDisabledError) 23 | jest 24 | .spyOn(Storage.prototype, 'getItem') 25 | .mockImplementation(throwDisabledError) 26 | } 27 | -------------------------------------------------------------------------------- /packages/browser/src/core/storage/memoryStorage.ts: -------------------------------------------------------------------------------- 1 | import { Store, StorageObject } from './types' 2 | 3 | /** 4 | * Data Storage using in memory object 5 | */ 6 | export class MemoryStorage 7 | implements Store 8 | { 9 | private cache: Record = {} 10 | 11 | get(key: K): Data[K] | null { 12 | return (this.cache[key] ?? null) as Data[K] | null 13 | } 14 | 15 | set(key: K, value: Data[K] | null): void { 16 | this.cache[key] = value 17 | } 18 | 19 | remove(key: K): void { 20 | delete this.cache[key] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/browser/src/core/storage/settings.ts: -------------------------------------------------------------------------------- 1 | import { StoreType, StoreTypeWithSettings } from './types' 2 | 3 | export type UniversalStorageSettings = { stores: StoreType[] } 4 | 5 | // This is setup this way to permit eventually a different set of settings for custom storage 6 | export type StorageSettings = UniversalStorageSettings 7 | 8 | export function isArrayOfStoreType( 9 | s: StorageSettings 10 | ): s is UniversalStorageSettings { 11 | return ( 12 | s && 13 | s.stores && 14 | Array.isArray(s.stores) && 15 | s.stores.every((e) => Object.values(StoreType).includes(e)) 16 | ) 17 | } 18 | 19 | export function isStoreTypeWithSettings( 20 | s: StoreTypeWithSettings | StoreType 21 | ): s is StoreTypeWithSettings { 22 | return typeof s === 'object' && s.name !== undefined 23 | } 24 | -------------------------------------------------------------------------------- /packages/browser/src/generated/__tests__/version.test.ts: -------------------------------------------------------------------------------- 1 | import { version } from '../version' 2 | import { readFileSync } from 'fs' 3 | import { resolve as resolvePath } from 'path' 4 | 5 | function getPackageJsonVersion(): string { 6 | const packageJson = JSON.parse( 7 | readFileSync( 8 | resolvePath(__dirname, '..', '..', '..', 'package.json') 9 | ).toString() 10 | ) 11 | return packageJson.version 12 | } 13 | 14 | describe('version', () => { 15 | it('matches version in package.json', async () => { 16 | expect(version).toBe(getPackageJsonVersion()) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /packages/browser/src/generated/version.ts: -------------------------------------------------------------------------------- 1 | // This file is generated. 2 | export const version = '1.81.0' 3 | -------------------------------------------------------------------------------- /packages/browser/src/lib/__tests__/embedded-write-key.test.ts: -------------------------------------------------------------------------------- 1 | import { embeddedWriteKey } from '../embedded-write-key' 2 | 3 | it('it guards against undefined', () => { 4 | expect(embeddedWriteKey()).toBe(undefined) 5 | }) 6 | 7 | it('it returns undefined when default parameter is set', () => { 8 | window.analyticsWriteKey = '__WRITE_KEY__' 9 | expect(embeddedWriteKey()).toBe(undefined) 10 | }) 11 | 12 | it('it returns the write key when the key is set properly', () => { 13 | window.analyticsWriteKey = 'abc_123_write_key' 14 | expect(embeddedWriteKey()).toBe('abc_123_write_key') 15 | }) 16 | -------------------------------------------------------------------------------- /packages/browser/src/lib/__tests__/get-process-env.test.ts: -------------------------------------------------------------------------------- 1 | import { getProcessEnv } from '../get-process-env' 2 | 3 | it('it matches the contents of process.env', () => { 4 | expect(getProcessEnv()).toBe(process.env) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/browser/src/lib/bind-all.ts: -------------------------------------------------------------------------------- 1 | export default function bindAll< 2 | ObjType extends { [key: string]: any }, 3 | KeyType extends keyof ObjType 4 | >(obj: ObjType): ObjType { 5 | const proto = obj.constructor.prototype 6 | for (const key of Object.getOwnPropertyNames(proto)) { 7 | if (key !== 'constructor') { 8 | const desc = Object.getOwnPropertyDescriptor( 9 | obj.constructor.prototype, 10 | key 11 | ) 12 | if (!!desc && typeof desc.value === 'function') { 13 | obj[key as KeyType] = obj[key].bind(obj) 14 | } 15 | } 16 | } 17 | 18 | return obj 19 | } 20 | -------------------------------------------------------------------------------- /packages/browser/src/lib/browser-polyfill.ts: -------------------------------------------------------------------------------- 1 | export function shouldPolyfill(): boolean { 2 | const browserVersionCompatList: { [browser: string]: number } = { 3 | Firefox: 46, 4 | Edge: 13, 5 | } 6 | 7 | // Unfortunately IE doesn't follow the same pattern as other browsers, so we 8 | // need to check `isIE11` differently. 9 | // @ts-expect-error 10 | const isIE11 = !!window.MSInputMethodContext && !!document.documentMode 11 | 12 | const userAgent = navigator.userAgent.split(' ') 13 | const [browser, version] = userAgent[userAgent.length - 1].split('/') 14 | 15 | return ( 16 | isIE11 || 17 | (browserVersionCompatList[browser] !== undefined && 18 | browserVersionCompatList[browser] >= parseInt(version)) 19 | ) 20 | } 21 | 22 | // appName = Netscape IE / Edge 23 | // edge 13 Edge/13... same as FF 24 | -------------------------------------------------------------------------------- /packages/browser/src/lib/client-hints/index.ts: -------------------------------------------------------------------------------- 1 | import { HighEntropyHint, NavigatorUAData, UADataValues } from './interfaces' 2 | 3 | export async function clientHints( 4 | hints?: HighEntropyHint[] 5 | ): Promise { 6 | const userAgentData = (navigator as any).userAgentData as 7 | | NavigatorUAData 8 | | undefined 9 | 10 | if (!userAgentData) return undefined 11 | 12 | if (!hints) return userAgentData.toJSON() 13 | return userAgentData 14 | .getHighEntropyValues(hints) 15 | .catch(() => userAgentData.toJSON()) 16 | } 17 | -------------------------------------------------------------------------------- /packages/browser/src/lib/csp-detection.ts: -------------------------------------------------------------------------------- 1 | import { loadScript } from './load-script' 2 | import { getLegacyAJSPath } from './parse-cdn' 3 | 4 | type CSPErrorEvent = SecurityPolicyViolationEvent & { 5 | disposition?: 'enforce' | 'report' 6 | } 7 | export const isAnalyticsCSPError = (e: CSPErrorEvent) => { 8 | return e.disposition !== 'report' && e.blockedURI.includes('cdn.segment') 9 | } 10 | 11 | export async function loadAjsClassicFallback(): Promise { 12 | console.warn( 13 | 'Your CSP policy is missing permissions required in order to run Analytics.js 2.0', 14 | 'https://segment.com/docs/connections/sources/catalog/libraries/website/javascript/upgrade-to-ajs2/#using-a-strict-content-security-policy-on-the-page' 15 | ) 16 | console.warn('Reverting to Analytics.js 1.0') 17 | 18 | const classicPath = getLegacyAJSPath() 19 | await loadScript(classicPath) 20 | } 21 | -------------------------------------------------------------------------------- /packages/browser/src/lib/embedded-write-key.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | analyticsWriteKey?: string 4 | } 5 | } 6 | 7 | // This variable is used as an optional fallback for when customers 8 | // host or proxy their own analytics.js. 9 | try { 10 | window.analyticsWriteKey = '__WRITE_KEY__' 11 | } catch (_) { 12 | // @ eslint-disable-next-line 13 | } 14 | 15 | export function embeddedWriteKey(): string | undefined { 16 | if (window.analyticsWriteKey === undefined) { 17 | return undefined 18 | } 19 | 20 | // this is done so that we don't accidentally override every reference to __write_key__ 21 | return window.analyticsWriteKey !== ['__', 'WRITE', '_', 'KEY', '__'].join('') 22 | ? window.analyticsWriteKey 23 | : undefined 24 | } 25 | -------------------------------------------------------------------------------- /packages/browser/src/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import unfetch from 'unfetch' 2 | import { getGlobal } from './get-global' 3 | 4 | /** 5 | * Wrapper around native `fetch` containing `unfetch` fallback. 6 | */ 7 | export const fetch: typeof global.fetch = (...args) => { 8 | const global = getGlobal() 9 | return ((global && global.fetch) || unfetch)(...args) 10 | } 11 | -------------------------------------------------------------------------------- /packages/browser/src/lib/get-global.ts: -------------------------------------------------------------------------------- 1 | // This an imperfect polyfill for globalThis 2 | export const getGlobal = () => { 3 | if (typeof globalThis !== 'undefined') { 4 | return globalThis 5 | } 6 | if (typeof self !== 'undefined') { 7 | return self 8 | } 9 | if (typeof window !== 'undefined') { 10 | return window 11 | } 12 | if (typeof global !== 'undefined') { 13 | return global 14 | } 15 | return null 16 | } 17 | -------------------------------------------------------------------------------- /packages/browser/src/lib/get-process-env.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns `process.env` if it is available in the environment. 3 | * Always returns an object make it similarly easy to use as `process.env`. 4 | */ 5 | export function getProcessEnv(): { [key: string]: string | undefined } { 6 | if (typeof process === 'undefined' || !process.env) { 7 | return {} 8 | } 9 | 10 | return process.env 11 | } 12 | -------------------------------------------------------------------------------- /packages/browser/src/lib/group-by.ts: -------------------------------------------------------------------------------- 1 | type Grouper = (obj: T) => string | number 2 | 3 | export function groupBy( 4 | collection: T[], 5 | grouper: keyof T | Grouper 6 | ): Record { 7 | const results: Record = {} 8 | 9 | collection.forEach((item) => { 10 | let key: string | number | undefined = undefined 11 | 12 | if (typeof grouper === 'string') { 13 | const suggestedKey = item[grouper] 14 | key = 15 | typeof suggestedKey !== 'string' 16 | ? JSON.stringify(suggestedKey) 17 | : suggestedKey 18 | } else if (grouper instanceof Function) { 19 | key = grouper(item) 20 | } 21 | 22 | if (key === undefined) { 23 | return 24 | } 25 | 26 | results[key] = [...(results[key] ?? []), item] 27 | }) 28 | 29 | return results 30 | } 31 | -------------------------------------------------------------------------------- /packages/browser/src/lib/is-plan-event-enabled.ts: -------------------------------------------------------------------------------- 1 | import { PlanEvent, TrackPlan } from '../core/events/interfaces' 2 | 3 | /** 4 | * Determines whether a track event is allowed to be sent based on the 5 | * user's tracking plan. 6 | * If the user does not have a tracking plan or the event is allowed based 7 | * on the tracking plan configuration, returns true. 8 | */ 9 | export function isPlanEventEnabled( 10 | plan: TrackPlan | undefined, 11 | planEvent: PlanEvent | undefined 12 | ): boolean { 13 | // Always prioritize the event's `enabled` status 14 | if (typeof planEvent?.enabled === 'boolean') { 15 | return planEvent.enabled 16 | } 17 | 18 | // Assume absence of a tracking plan means events are enabled 19 | return plan?.__default?.enabled ?? true 20 | } 21 | -------------------------------------------------------------------------------- /packages/browser/src/lib/is-thenable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if thenable 3 | * (instanceof Promise doesn't respect realms) 4 | */ 5 | export const isThenable = (value: unknown): boolean => 6 | typeof value === 'object' && 7 | value !== null && 8 | 'then' in value && 9 | typeof (value as any).then === 'function' 10 | -------------------------------------------------------------------------------- /packages/browser/src/lib/p-while.ts: -------------------------------------------------------------------------------- 1 | export const pWhile = async ( 2 | condition: (value: T | undefined) => boolean, 3 | action: () => T | PromiseLike 4 | ): Promise => { 5 | const loop = async (actionResult: T | undefined): Promise => { 6 | if (condition(actionResult)) { 7 | return loop(await action()) 8 | } 9 | } 10 | 11 | return loop(undefined) 12 | } 13 | -------------------------------------------------------------------------------- /packages/browser/src/lib/pick.ts: -------------------------------------------------------------------------------- 1 | export function pick, K extends keyof T>( 2 | object: T, 3 | keys: readonly K[] 4 | ): Pick 5 | 6 | export function pick>( 7 | object: T, 8 | keys: string[] 9 | ): Partial 10 | 11 | /** 12 | * @example 13 | * pick({ 'a': 1, 'b': '2', 'c': 3 }, ['a', 'c']) 14 | * => { 'a': 1, 'c': 3 } 15 | */ 16 | export function pick, K extends keyof T>( 17 | object: T, 18 | keys: string[] | K[] | readonly K[] 19 | ) { 20 | return Object.assign( 21 | {}, 22 | ...keys.map((key) => { 23 | if (object && Object.prototype.hasOwnProperty.call(object, key)) { 24 | return { [key]: object[key] } 25 | } 26 | }) 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/browser/src/lib/priority-queue/backoff.ts: -------------------------------------------------------------------------------- 1 | type BackoffParams = { 2 | /** The number of milliseconds before starting the first retry. Default is 500 */ 3 | minTimeout?: number 4 | 5 | /** The maximum number of milliseconds between two retries. Default is Infinity */ 6 | maxTimeout?: number 7 | 8 | /** The exponential factor to use. Default is 2. */ 9 | factor?: number 10 | 11 | /** The current attempt */ 12 | attempt: number 13 | } 14 | 15 | export function backoff(params: BackoffParams): number { 16 | const random = Math.random() + 1 17 | const { 18 | minTimeout = 500, 19 | factor = 2, 20 | attempt, 21 | maxTimeout = Infinity, 22 | } = params 23 | return Math.min(random * minTimeout * Math.pow(factor, attempt), maxTimeout) 24 | } 25 | -------------------------------------------------------------------------------- /packages/browser/src/lib/priority-queue/index.ts: -------------------------------------------------------------------------------- 1 | import { PriorityQueue, ON_REMOVE_FROM_FUTURE } from '@segment/analytics-core' 2 | 3 | export { PriorityQueue, ON_REMOVE_FROM_FUTURE } 4 | -------------------------------------------------------------------------------- /packages/browser/src/lib/sleep.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (time: number): Promise => 2 | new Promise((resolve) => { 3 | setTimeout(resolve, time) 4 | }) 5 | -------------------------------------------------------------------------------- /packages/browser/src/lib/version-type.ts: -------------------------------------------------------------------------------- 1 | // Default value will be updated to 'web' in `bundle-umd.ts` for web build. 2 | let _version: 'web' | 'npm' = 'npm' 3 | 4 | export function setVersionType(version: typeof _version) { 5 | _version = version 6 | } 7 | 8 | export function getVersionType(): typeof _version { 9 | return _version 10 | } 11 | -------------------------------------------------------------------------------- /packages/browser/src/node/__tests__/node-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { AnalyticsNode } from '../..' 2 | 3 | const writeKey = 'foo' 4 | 5 | describe('Initialization', () => { 6 | it('loads analytics-node-next plugin', async () => { 7 | const [analytics] = await AnalyticsNode.load({ 8 | writeKey, 9 | }) 10 | 11 | expect(analytics.queue.plugins.length).toBe(1) 12 | 13 | const ajsNodeXt = analytics.queue.plugins.find( 14 | (xt) => xt.name === 'analytics-node-next' 15 | ) 16 | expect(ajsNodeXt).toBeDefined() 17 | expect(ajsNodeXt?.isLoaded()).toBeTruthy() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /packages/browser/src/node/node.browser.ts: -------------------------------------------------------------------------------- 1 | export class AnalyticsNode { 2 | static load(): Promise { 3 | return Promise.reject( 4 | new Error('AnalyticsNode is not available in browsers.') 5 | ) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/browser/src/plugins/legacy-video-plugins/index.ts: -------------------------------------------------------------------------------- 1 | import { Analytics } from '../../core/analytics' 2 | 3 | export async function loadLegacyVideoPlugins( 4 | analytics: Analytics 5 | ): Promise { 6 | const plugins = await import( 7 | // @ts-expect-error 8 | '@segment/analytics.js-video-plugins/dist/index.umd.js' 9 | ) 10 | 11 | // This is super gross, but we need to support the `window.analytics.plugins` namespace 12 | // that is linked in the segment docs in order to be backwards compatible with ajs-classic 13 | 14 | // @ts-expect-error 15 | analytics._plugins = plugins 16 | } 17 | -------------------------------------------------------------------------------- /packages/browser/src/plugins/segmentio/ratelimit-error.ts: -------------------------------------------------------------------------------- 1 | export class RateLimitError extends Error { 2 | retryTimeout: number 3 | 4 | constructor(message: string, retryTimeout: number) { 5 | super(message) 6 | this.retryTimeout = retryTimeout 7 | this.name = 'RateLimitError' 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/browser/src/test-helpers/factories.ts: -------------------------------------------------------------------------------- 1 | export const createSuccess = (body: any, overrides: Partial = {}) => { 2 | return Promise.resolve({ 3 | json: () => Promise.resolve(body), 4 | ok: true, 5 | status: 200, 6 | statusText: 'OK', 7 | ...overrides, 8 | }) as Promise 9 | } 10 | 11 | export const createError = (overrides: Partial = {}) => { 12 | return Promise.resolve({ 13 | ok: false, 14 | status: 404, 15 | statusText: 'Not Found', 16 | ...overrides, 17 | }) as Promise 18 | } 19 | -------------------------------------------------------------------------------- /packages/browser/src/test-helpers/fetch-parse.ts: -------------------------------------------------------------------------------- 1 | export type FetchCall = [input: RequestInfo, init?: RequestInit | undefined] 2 | 3 | export const parseFetchCall = ([url, request]: FetchCall) => ({ 4 | url, 5 | method: request?.method, 6 | headers: request?.headers, 7 | body: request?.body ? JSON.parse(request.body as any) : undefined, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/browser/src/test-helpers/fixtures/classic-destination.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | /** 3 | * This is a classic destination, such as: 4 | * https://github.com/segmentio/analytics.js-integrations/blob/master/integrations/appcues/lib/index.js 5 | */ 6 | 7 | const integration = require('@segment/analytics.js-integration') 8 | 9 | export const mockIntegrationName = 'Fake' 10 | export const Fake = integration(mockIntegrationName) 11 | 12 | Fake.prototype.initialize = function () { 13 | this.load(this.ready) 14 | } 15 | 16 | Fake.prototype.loaded = function () { 17 | return true 18 | } 19 | 20 | Fake.prototype.track = function () {} 21 | 22 | Fake.prototype.load = function (callback: Function) { 23 | // this callback is important to actually initialize. 24 | callback() 25 | } 26 | -------------------------------------------------------------------------------- /packages/browser/src/test-helpers/fixtures/client-hints.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UADataValues, 3 | UALowEntropyJSON, 4 | } from '../../lib/client-hints/interfaces' 5 | 6 | export const lowEntropyTestData: UALowEntropyJSON = { 7 | brands: [ 8 | { 9 | brand: 'Google Chrome', 10 | version: '113', 11 | }, 12 | { 13 | brand: 'Chromium', 14 | version: '113', 15 | }, 16 | { 17 | brand: 'Not-A.Brand', 18 | version: '24', 19 | }, 20 | ], 21 | mobile: false, 22 | platform: 'macOS', 23 | } 24 | 25 | export const highEntropyTestData: UADataValues = { 26 | architecture: 'x86', 27 | bitness: '64', 28 | } 29 | -------------------------------------------------------------------------------- /packages/browser/src/test-helpers/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | export * from './page-context' 2 | export * from './create-fetch-method' 3 | export * from './classic-destination' 4 | export * from './cdn-settings' 5 | -------------------------------------------------------------------------------- /packages/browser/src/test-helpers/fixtures/page-context.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type BufferedPageContext, 3 | type PageContext, 4 | getDefaultBufferedPageContext, 5 | getDefaultPageContext, 6 | } from '../../core/page' 7 | 8 | export const getPageCtxFixture = (): PageContext => getDefaultPageContext() 9 | 10 | export const getBufferedPageCtxFixture = (): BufferedPageContext => 11 | getDefaultBufferedPageContext() 12 | -------------------------------------------------------------------------------- /packages/browser/src/test-helpers/helpers.ts: -------------------------------------------------------------------------------- 1 | import { sleep } from '@segment/analytics-core' 2 | 3 | export const waitForCondition = async ( 4 | condition: () => boolean, 5 | timeout = 1000 6 | ): Promise => { 7 | const start = Date.now() 8 | while (!condition()) { 9 | if (Date.now() - start > timeout) { 10 | throw new Error(`Timeout of ${timeout}ms exceeded!`) 11 | } 12 | await sleep(10) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/browser/src/test-helpers/test-writekeys.ts: -------------------------------------------------------------------------------- 1 | // Writekeys that are used in different unit/integration tests. These writekeys 2 | // were created for the purpose of testing AJS and are not used for anything else. 3 | 4 | export const TEST_WRITEKEY = 'D8frB7upBChqDN9PMWksNvZYDaKJIYo6' // Test segment writekey 5 | export const AMPLITUDE_WRITEKEY = 'c56fd8ca27d0f9adfe8ad78d846dfcc8' // Test amplitude writekey 6 | -------------------------------------------------------------------------------- /packages/browser/src/test-helpers/type-assertions.ts: -------------------------------------------------------------------------------- 1 | type IsAny = unknown extends T ? (T extends {} ? T : never) : never 2 | type NotAny = T extends IsAny ? never : T 3 | type NotUnknown = unknown extends T ? never : T 4 | 5 | type NotTopType = NotAny & NotUnknown 6 | 7 | // this is not meant to be run, just for type tests 8 | export function assertNotAny(_val: NotTopType) {} 9 | 10 | // this is not meant to be run, just for type tests 11 | export function assertIs(_val: T) {} 12 | -------------------------------------------------------------------------------- /packages/browser/src/tester/__fixtures__/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 🍻 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/browser/src/tester/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const express = require('express') 3 | const port = 3001 4 | 5 | const app = express() 6 | 7 | app.get('*.js', function (req, res, next) { 8 | req.url = req.url + '.gz' 9 | res.set('Content-Encoding', 'gzip') 10 | res.set('Content-Type', 'text/javascript') 11 | next() 12 | }) 13 | app.use('/', express.static(path.join(__dirname, '__fixtures__'))) 14 | app.use('/dist/umd', express.static(path.join(__dirname, '../../dist/umd'))) 15 | 16 | app.listen(port) 17 | -------------------------------------------------------------------------------- /packages/browser/src/vendor/tsub/types.ts: -------------------------------------------------------------------------------- 1 | export interface Rule { 2 | scope: string 3 | target_type: string 4 | matchers: Matcher[] 5 | transformers: Transformer[][] 6 | destinationName?: string 7 | } 8 | export interface Matcher { 9 | type: string 10 | ir: string 11 | } 12 | export interface Transformer { 13 | type: string 14 | config?: any 15 | } 16 | export interface Store { 17 | new (rules?: Rule[]): this 18 | getRulesByDestinationName(destinationName: string): Rule[] 19 | } 20 | -------------------------------------------------------------------------------- /packages/browser/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["**/__tests__/**", "**/test-helpers/**", "**/tester/**"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "outDir": "./dist/pkg", 8 | "declarationDir": "./dist/types" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/browser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "ES5", 6 | "moduleResolution": "node", 7 | "resolveJsonModule": true, 8 | "lib": ["es2020", "DOM", "DOM.Iterable"], 9 | "baseUrl": "./src", 10 | "keyofStringsOnly": true 11 | }, 12 | "exclude": ["node_modules", "dist"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/config-tsup/config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('tsup') 2 | 3 | module.exports = defineConfig(() => ({ 4 | entry: ['src/index.ts'], 5 | format: ['esm', 'cjs'], // Output both ESM and CJS formats 6 | dts: { 7 | resolve: true, 8 | entry: 'src/index.ts', 9 | }, 10 | clean: true, 11 | esbuildOptions(options, context) { 12 | if (context.format === 'esm') { 13 | options.outdir = 'dist/esm' // Output ESM files to dist/esm 14 | options.outExtension = { '.js': '.mjs' } // Use .mjs for ESM files 15 | } else if (context.format === 'cjs') { 16 | options.outdir = 'dist/cjs' // Output CJS files to dist/cjs 17 | } 18 | }, 19 | })) 20 | -------------------------------------------------------------------------------- /packages/config-tsup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/config-tsup", 3 | "version": "0.0.0", 4 | "private": true, 5 | "packageManager": "yarn@3.4.1", 6 | "devDependencies": { 7 | "tsup": "^8.4.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/config-webpack/README.md: -------------------------------------------------------------------------------- 1 | # @internal/config-webpack 2 | 3 | This package is for sharing basic webpack configuration / browser support between all of the analytics.js artifacts in this monorepo. 4 | -------------------------------------------------------------------------------- /packages/config-webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/config-webpack", 3 | "version": "0.0.0", 4 | "private": true, 5 | "packageManager": "yarn@3.4.1", 6 | "devDependencies": { 7 | "@babel/cli": "^7.22.10", 8 | "@babel/core": "^7.22.11", 9 | "@babel/preset-env": "^7.22.10", 10 | "@babel/preset-typescript": "^7.22.11", 11 | "@types/circular-dependency-plugin": "^5", 12 | "babel-loader": "^8.0.0", 13 | "circular-dependency-plugin": "^5.2.2", 14 | "ecma-version-validator-webpack-plugin": "^1.2.1", 15 | "terser-webpack-plugin": "^5.1.4", 16 | "webpack": "^5.94.0", 17 | "webpack-cli": "^4.8.0", 18 | "webpack-merge": "^5.9.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./src", 6 | "packageManager": "yarn@3.4.1", 7 | "devDependencies": { 8 | "app-root-path": "^3.1.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/config/src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | createJestTSConfig: require('./jest/config').createJestTSConfig, 3 | lintStagedConfig: require('./lint-staged/config'), 4 | } 5 | -------------------------------------------------------------------------------- /packages/config/src/lint-staged/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{js,jsx,ts,tsx}': ['eslint --fix'], 3 | '*.json*': ['prettier --write'], 4 | } 5 | -------------------------------------------------------------------------------- /packages/consent/README.md: -------------------------------------------------------------------------------- 1 | # Consent Management for Analytics.js 2 | ## Packages 3 | - [@segment/analytics-consent-tools](/packages/consent/consent-tools) - A library for integrating analytics with consent management platforms 4 | - [@segment/analytics-consent-wrapper-onetrust](/packages/consent/consent-wrapper-onetrust) - this is a library for using the OneTrust consent management platform with analytics.js 5 | 6 | ## Development 7 | 8 | ### Watch all consent packages _and_ their dependencies, building on change. 9 | ```sh 10 | # from repo root 11 | yarn turbo run watch --filter './packages/consent/*...' 12 | ``` 13 | 14 | ### Run tests (does not require active watch, since it uses ts-jest under the hood which compiles TS in memory) 15 | ```sh 16 | # from repo root 17 | yarn turbo run test --filter './packages/consent/*' 18 | ``` 19 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../../.eslintrc'], 4 | env: { 5 | browser: true, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/.gitignore: -------------------------------------------------------------------------------- 1 | driver-logs 2 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/README.md: -------------------------------------------------------------------------------- 1 | # @internal/consent-tools-integration-tests" 2 | 3 | ## Project structure 4 | - `/public` - Test server root 5 | - `/dist` - Holds the webpacked page-bundles that will be injected into each html page 6 | 7 | - `/src` - Test suite files 8 | - `/page-bundles` - For testing libraries that don't have a UMD bundle (i.e analytics-consent-tools) 9 | - `/page-objects` - Page objects for the test suite 10 | - `/page-tests ` - Tests that will be run on the page 11 | 12 | ## Development 13 | ### Build, start server, run tests (and exit gracefully) 14 | ``` 15 | yarn . test:int 16 | ``` 17 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/playwright.global-setup.ts: -------------------------------------------------------------------------------- 1 | import type { FullConfig } from '@playwright/test' 2 | import { execSync } from 'child_process' 3 | 4 | export default function globalSetup(_cfg: FullConfig) { 5 | console.log('Executing global setup...') 6 | execSync('yarn build', { stdio: 'inherit' }) 7 | console.log('Finished global setup.') 8 | } 9 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/public/consent-tools-vanilla-opt-in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Hello World - Serving Analytics

6 |

Please Check Network tab

7 |

This page can used as playground or run by Playwright

8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/public/consent-tools-vanilla-opt-out.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Hello World - Serving Analytics (Consent Tools Vanilla Opt Out)

6 |

Please Check Network tab

7 |

This page can used as playground or run by Playwright

8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/src/page-bundles/consent-tools-vanilla-opt-in/index.ts: -------------------------------------------------------------------------------- 1 | import { AnalyticsBrowser } from '@segment/analytics-next' 2 | import { initMockConsentManager } from '../helpers/mock-cmp' 3 | import { withMockCMP } from '../helpers/mock-cmp-wrapper' 4 | 5 | initMockConsentManager({ consentModel: 'opt-in' }) 6 | 7 | const analytics = new AnalyticsBrowser() 8 | 9 | // for testing 10 | ;(window as any).analytics = analytics 11 | 12 | withMockCMP(analytics).load({ 13 | writeKey: 'foo', 14 | }) 15 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/src/page-bundles/consent-tools-vanilla-opt-out/index.ts: -------------------------------------------------------------------------------- 1 | import { AnalyticsBrowser } from '@segment/analytics-next' 2 | import { initMockConsentManager } from '../helpers/mock-cmp' 3 | import { withMockCMP } from '../helpers/mock-cmp-wrapper' 4 | 5 | initMockConsentManager({ 6 | consentModel: 'opt-out', 7 | }) 8 | 9 | const analytics = new AnalyticsBrowser() 10 | 11 | withMockCMP(analytics).load( 12 | { 13 | writeKey: 'foo', 14 | }, 15 | { initialPageview: true } 16 | ) 17 | ;(window as any).analytics = analytics 18 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/src/page-bundles/onetrust/index.ts: -------------------------------------------------------------------------------- 1 | import { AnalyticsBrowser } from '@segment/analytics-next' 2 | import { withOneTrust } from '@segment/analytics-consent-wrapper-onetrust' 3 | 4 | const analytics = new AnalyticsBrowser() 5 | 6 | withOneTrust(analytics).load( 7 | { 8 | writeKey: 'foo', 9 | }, 10 | { initialPageview: true } 11 | ) 12 | ;(window as any).analytics = analytics 13 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/src/page-objects/consent-tools-vanilla.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@playwright/test' 2 | import { BasePage } from './base-page' 3 | 4 | export class ConsentToolsVanilla extends BasePage { 5 | constructor(page: Page, file: string) { 6 | super(page, file) 7 | } 8 | 9 | async clickGiveConsent() { 10 | await this.page.locator('#give-consent').click() 11 | } 12 | 13 | async clickDenyConsent() { 14 | await this.page.locator('#deny-consent').click() 15 | } 16 | } 17 | 18 | export class ConsentToolsVanillaOptOut extends ConsentToolsVanilla { 19 | constructor(page: Page) { 20 | super(page, 'consent-tools-vanilla-opt-out.html') 21 | } 22 | } 23 | 24 | export class ConsentToolsVanillaOptIn extends ConsentToolsVanilla { 25 | constructor(page: Page) { 26 | super(page, 'consent-tools-vanilla-opt-in.html') 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/src/tests/onetrust.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | import OneTrustPage from '../page-objects/onetrust' 3 | test.describe('OneTrust Consent Tests', () => { 4 | let pageObject: OneTrustPage 5 | 6 | test.beforeEach(async ({ page }) => { 7 | pageObject = new OneTrustPage(page) 8 | await pageObject.load() 9 | }) 10 | 11 | test.afterEach(async () => { 12 | await pageObject.cleanup() 13 | }) 14 | 15 | test('should send a consent changed event when user clicks accept on popup', async () => { 16 | expect(pageObject.getConsentChangedEvents().length).toBe(0) 17 | await pageObject.openAlertBoxIfNeeded() 18 | await pageObject.clickConfirmButtonAndClosePopup() 19 | await expect.poll(() => pageObject.getConsentChangedEvents().length).toBe(1) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/src/types/analytics.d.ts: -------------------------------------------------------------------------------- 1 | import type { AnalyticsSnippet } from '@segment/analytics-next' 2 | 3 | declare global { 4 | interface Window { 5 | analytics: AnalyticsSnippet 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/consent/consent-tools-integration-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "module": "ESNext", 6 | "target": "es6", 7 | "noEmit": true, 8 | "types": ["node"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../../.eslintrc'], 4 | env: { 5 | browser: true, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname, { 4 | testEnvironment: 'jsdom', 5 | setupFilesAfterEnv: ['./jest.setup.js'], 6 | }) 7 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/jest.setup.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | 3 | // eslint-disable-next-line no-undef 4 | globalThis.fetch = fetch // polyfill fetch so nock will work correctly on node 18 (https://github.com/nock/nock/issues/2336) 5 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/domain/__tests__/typedef-tests.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AnalyticsSnippet, 3 | AnalyticsBrowser, 4 | } from '@segment/analytics-next' 5 | import { createWrapper, AnyAnalytics } from '../../index' 6 | 7 | type Extends = T extends U ? true : false 8 | 9 | { 10 | const wrap = createWrapper({ getCategories: () => ({ foo: true }) }) 11 | wrap({} as AnalyticsBrowser) 12 | wrap({} as AnalyticsSnippet) 13 | 14 | // see AnalyticsSnippet and AnalyticsBrowser extend AnyAnalytics 15 | const f: Extends = true 16 | const g: Extends = true 17 | console.log(f, g) 18 | 19 | // should be chainable 20 | wrap({} as AnalyticsBrowser).load({ writeKey: 'foo' }) 21 | } 22 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/domain/analytics/index.ts: -------------------------------------------------------------------------------- 1 | export { AnalyticsService } from './analytics-service' 2 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/domain/consent-stamping.ts: -------------------------------------------------------------------------------- 1 | import { Categories, SourceMiddlewareFunction } from '../types' 2 | 3 | type CreateConsentMw = ( 4 | getCategories: () => Promise 5 | ) => SourceMiddlewareFunction 6 | 7 | /** 8 | * Create analytics addSourceMiddleware fn that stamps each event 9 | */ 10 | export const createConsentStampingMiddleware: CreateConsentMw = ( 11 | getCategories 12 | ) => { 13 | const fn: SourceMiddlewareFunction = async ({ payload, next }) => { 14 | payload.obj.context.consent = { 15 | ...payload.obj.context.consent, 16 | categoryPreferences: await getCategories(), 17 | } 18 | 19 | next(payload) 20 | } 21 | return fn 22 | } 23 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/domain/logger.ts: -------------------------------------------------------------------------------- 1 | // hacky debug.ts, can be replaced with a proper logging solution 2 | // @ts-ignore 3 | 4 | class Logger { 5 | private get debugLoggingEnabled() { 6 | return (window as any)['SEGMENT_CONSENT_WRAPPER_DEBUG_MODE'] === true 7 | } 8 | 9 | enableDebugLogging() { 10 | ;(window as any)['SEGMENT_CONSENT_WRAPPER_DEBUG_MODE'] = true 11 | } 12 | 13 | debug(...args: any[]): void { 14 | if (this.debugLoggingEnabled) { 15 | console.log('[consent wrapper debug]', ...args) 16 | } 17 | } 18 | } 19 | 20 | export const logger = new Logger() 21 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/domain/pruned-categories.ts: -------------------------------------------------------------------------------- 1 | import { pick } from '../utils' 2 | import { Categories } from '../types' 3 | import { ValidationError } from './validation/validation-error' 4 | 5 | export const getPrunedCategories = ( 6 | categories: Categories, 7 | allCategories: string[] 8 | ): Categories => { 9 | if (!allCategories.length) { 10 | // No configured integrations found, so no categories will be sent (should not happen unless there's a configuration error) 11 | throw new ValidationError( 12 | 'Invariant: No consent categories defined in Segment', 13 | [] 14 | ) 15 | } 16 | 17 | return pick(categories, allCategories) 18 | } 19 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/domain/validation/__tests__/validation-error.test.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from '../validation-error' 2 | 3 | describe(ValidationError, () => { 4 | it('should have the correct shape', () => { 5 | const err = new ValidationError('foo', 'bar') 6 | 7 | expect(err).toBeInstanceOf(Error) 8 | 9 | expect(err.name).toBe('ValidationError') 10 | 11 | expect(err.message).toMatchInlineSnapshot( 12 | `"[Validation] foo (Received: "bar")"` 13 | ) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/domain/validation/common-validators.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from './validation-error' 2 | 3 | export function assertIsFunction( 4 | val: unknown, 5 | variableName: string 6 | ): asserts val is Function { 7 | if (typeof val !== 'function') { 8 | throw new ValidationError(`${variableName} is not a function`, val) 9 | } 10 | } 11 | 12 | export function assertIsObject( 13 | val: unknown, 14 | variableName: string 15 | ): asserts val is object { 16 | if (val === null || typeof val !== 'object') { 17 | throw new ValidationError(`${variableName} is not an object`, val) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/domain/validation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './options-validators' 2 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/domain/validation/validation-error.ts: -------------------------------------------------------------------------------- 1 | import { AnalyticsConsentError } from '../../types/errors' 2 | 3 | export class ValidationError extends AnalyticsConsentError { 4 | constructor(message: string, received?: any) { 5 | if (arguments.length === 2) { 6 | // to ensure that explicitly passing undefined as second argument still works 7 | message += ` (Received: ${JSON.stringify(received)})` 8 | } 9 | super('ValidationError', `[Validation] ${message}`) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the public API for this package. 3 | * We avoid using splat (*) exports so that we can control what is exposed. 4 | */ 5 | export { createWrapper } from './domain/create-wrapper' 6 | export { resolveWhen } from './utils' 7 | export type { ConsentModel } from './domain/load-context' 8 | 9 | export type { 10 | Wrapper, 11 | CreateWrapper, 12 | CreateWrapperSettings, 13 | IntegrationCategoryMappings, 14 | Categories, 15 | GetCategoriesFunction, 16 | RegisterOnConsentChangedFunction, 17 | AnyAnalytics, 18 | } from './types' 19 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/test-helpers/mocks/analytics-mock.ts: -------------------------------------------------------------------------------- 1 | import { AnyAnalytics } from '../../types' 2 | 3 | export const analyticsMock: jest.Mocked = { 4 | addDestinationMiddleware: jest.fn(), 5 | addSourceMiddleware: jest.fn(), 6 | page: jest.fn(), 7 | load: jest.fn(), 8 | track: jest.fn(), 9 | } 10 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/test-helpers/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './analytics-mock' 2 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/types/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base Consent Error 3 | */ 4 | export abstract class AnalyticsConsentError extends Error { 5 | /** 6 | * 7 | * @param name - Pass the name explicitly to work around the limitation that 'name' is automatically set to the parent class. 8 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends#using_extends 9 | * @param message - Error message 10 | */ 11 | constructor(public name: string, message: string) { 12 | super(message) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './wrapper' 2 | export * from './settings' 3 | export * from './errors' 4 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pipe' 2 | export * from './resolve-when' 3 | export * from './uniq' 4 | export * from './pick' 5 | export * from './ts-helpers' 6 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/utils/pick.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 3 | * pick({ a: 1, b: 2, c: 3 }, ['a', 'c']) => { a: 1, c: 3 } 4 | */ 5 | export const pick = < 6 | Obj extends Record, 7 | Key extends keyof Obj 8 | >( 9 | obj: Obj, 10 | keys: Key[] 11 | ): Pick => { 12 | return keys.reduce((acc, k) => { 13 | if (k in obj) { 14 | acc[k] = obj[k] 15 | } 16 | return acc 17 | }, {} as Pick) 18 | } 19 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/utils/pipe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Everyday variadic pipe function (reverse of 'compose') 3 | * @example pipe(fn1, fn2, fn3)(value) // fn3(fn2(fn1(value))) 4 | */ 5 | export const pipe = ( 6 | fn: (...args: T) => U, 7 | ...fns: ((a: U) => U)[] 8 | ) => { 9 | const piped = fns.reduce( 10 | (prevFn, nextFn) => (value: U) => nextFn(prevFn(value)), 11 | (value) => value 12 | ) 13 | return (...args: T) => piped(fn(...args)) 14 | } 15 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/src/utils/uniq.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This function removes duplicates from an array 3 | */ 4 | export const uniq = (arr: T[]): T[] => Array.from(new Set(arr)) 5 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["**/__tests__/**"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "outDir": "./dist/esm", 8 | "declarationDir": "./dist/types" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/consent/consent-tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "exclude": ["node_modules", "dist"], 4 | "compilerOptions": { 5 | "module": "ESNext", // es6 modules 6 | "target": "ES2020", // don't down-compile *too much* -- if users are using webpack, they can always transpile this library themselves 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], // assume that consumers will be polyfilling at least down to es2020 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true // ensure we are friendly to build systems 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../../.eslintrc'], 4 | env: { 5 | browser: true, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/img/consent-mgmt-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-next/c0d58e3fc12e9733eacfac7619d7369a2e297d35/packages/consent/consent-wrapper-onetrust/img/consent-mgmt-ui.png -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/img/onetrust-cat-id.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-next/c0d58e3fc12e9733eacfac7619d7369a2e297d35/packages/consent/consent-wrapper-onetrust/img/onetrust-cat-id.jpg -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/img/onetrust-popup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-next/c0d58e3fc12e9733eacfac7619d7369a2e297d35/packages/consent/consent-wrapper-onetrust/img/onetrust-popup.jpg -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname, { 4 | testEnvironment: 'jsdom', 5 | setupFilesAfterEnv: ['./jest.setup.js'], 6 | }) 7 | -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/jest.setup.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | const { TextEncoder, TextDecoder } = require('util') 3 | 4 | // fix: "ReferenceError: TextEncoder is not defined" after upgrading JSDOM 5 | global.TextEncoder = TextEncoder 6 | global.TextDecoder = TextDecoder 7 | 8 | // eslint-disable-next-line no-undef 9 | globalThis.fetch = fetch // polyfill fetch so nock will work correctly on node 18 (https://github.com/nock/nock/issues/2336) 10 | -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the public API for this package. 3 | * We avoid using splat (*) exports so that we can control what is exposed. 4 | */ 5 | export { withOneTrust } from './domain/wrapper' 6 | export type { OneTrustSettings } from './domain/wrapper' 7 | -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/src/index.umd.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is meant to be used to create a webpack bundle. 3 | */ 4 | import { withOneTrust } from './index' 5 | export { withOneTrust } 6 | 7 | // this will almost certainly be executed in the browser, but since this is UMD, 8 | // we are checking just for the sake of being thorough 9 | if (typeof window !== 'undefined') { 10 | ;(window as any).withOneTrust = withOneTrust 11 | } 12 | -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/src/lib/validation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './onetrust-api-error' 2 | -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/src/lib/validation/onetrust-api-error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An Errot that represents that the OneTrust API is not in the expected format. 3 | * This is not something that could happen unless our API types are wrong and something is very wonky. 4 | * Not a recoverable error. 5 | */ 6 | export class OneTrustApiValidationError extends Error { 7 | name = 'OtConsentWrapperValidationError' 8 | constructor(message: string, received: any) { 9 | super(`Invariant: ${message} (Received: ${JSON.stringify(received)})`) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/src/test-helpers/onetrust-globals.d.ts: -------------------------------------------------------------------------------- 1 | import { OneTrustGlobal } from '../lib/onetrust-api' 2 | /** 3 | * ALERT: It's OK to declare ambient globals in test code, but __not__ in library code 4 | * This file should not be included in the final package 5 | */ 6 | export declare global { 7 | interface Window { 8 | OneTrust: OneTrustGlobal 9 | OnetrustActiveGroups: string 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/src/test-helpers/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows mocked objects to throw a helpful error message when a method is called without an implementation 3 | */ 4 | export const addDebugMockImplementation = (mock: jest.Mocked) => { 5 | Object.entries(mock).forEach(([method, value]) => { 6 | // automatically add mock implementation for debugging purposes 7 | if (typeof value === 'function') { 8 | mock[method] = mock[method].mockImplementation((...args: any[]) => { 9 | throw new Error(`Not Implemented: ${method}(${JSON.stringify(args)})`) 10 | }) 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["**/__tests__/**", "**/test-helpers/**"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "outDir": "./dist/esm", 8 | "declarationDir": "./dist/types" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/consent/consent-wrapper-onetrust/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "exclude": ["node_modules", "dist"], 4 | "compilerOptions": { 5 | "module": "ESNext", // es6 modules 6 | "target": "ES2020", // don't down-compile *too much* -- if users are using webpack, they can always transpile this library themselves 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], // assume that consumers will be polyfilling at least down to es2020 8 | "moduleResolution": "node", 9 | "isolatedModules": true // ensure we are friendly to build systems 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core-integration-tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../.eslintrc'], 4 | } 5 | -------------------------------------------------------------------------------- /packages/core-integration-tests/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/core-integration-tests/README.md: -------------------------------------------------------------------------------- 1 | # Core Integration Tests 2 | This can contain a mix of tests which cover the public API of the package. This can range anywhere from typical integration tests that might stub out the API (which may or may not also be in the package itself), to tests around the specific npm packaged artifact. Examples include: 3 | - Is a license included in npm pack? 4 | - can you import a module (e.g. is the package.json path correctly to allow consumers to import)? 5 | - are there missing depenndencies in package.json? 6 | -------------------------------------------------------------------------------- /packages/core-integration-tests/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname) 4 | -------------------------------------------------------------------------------- /packages/core-integration-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/core-integration-tests", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "yarn jest", 7 | "lint": "yarn concurrently 'yarn:eslint .' 'yarn:tsc --noEmit'", 8 | "watch:test": "yarn test --watch", 9 | "tsc": "yarn run -T tsc", 10 | "eslint": "yarn run -T eslint", 11 | "concurrently": "yarn run -T concurrently", 12 | "jest": "yarn run -T jest" 13 | }, 14 | "packageManager": "yarn@3.4.1", 15 | "devDependencies": { 16 | "@internal/config": "workspace:^", 17 | "@segment/analytics-core": "workspace:^" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/core-integration-tests/src/public-api.test.ts: -------------------------------------------------------------------------------- 1 | import { CoreContext } from '@segment/analytics-core' 2 | 3 | class TestCtx extends CoreContext {} 4 | 5 | it('should be able to import and instantiate some module from core', () => { 6 | // Test the ability to do basic imports 7 | expect(typeof new TestCtx({ type: 'alias' })).toBe('object') 8 | }) 9 | -------------------------------------------------------------------------------- /packages/core-integration-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "exclude": ["node_modules", "dist"], 4 | "compilerOptions": { 5 | "noUnusedLocals": false, 6 | "noUnusedParameters": false, 7 | "module": "esnext", 8 | "target": "ES5", 9 | "moduleResolution": "node", 10 | "lib": ["es2020"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../.eslintrc.isomorphic'], 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @segment/analytics-core 2 | 3 | This package represents core 'shared' functionality that is shared by analytics packages. This is not designed to be used directly, but internal to analytics-node and analytics-browser. 4 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname) 4 | -------------------------------------------------------------------------------- /packages/core/jest.setup.js: -------------------------------------------------------------------------------- 1 | const { TextEncoder, TextDecoder } = require('util') 2 | const { setImmediate } = require('timers') 3 | 4 | // fix: "ReferenceError: TextEncoder is not defined" after upgrading JSDOM 5 | global.TextEncoder = TextEncoder 6 | global.TextDecoder = TextDecoder 7 | // fix: jsdom uses setImmediate under the hood for preflight XHR requests, 8 | // and jest removed setImmediate, so we need to provide it to prevent console 9 | // logging ReferenceErrors made by integration tests that call Amplitude. 10 | global.setImmediate = setImmediate 11 | -------------------------------------------------------------------------------- /packages/core/src/analytics/index.ts: -------------------------------------------------------------------------------- 1 | export interface CoreAnalytics { 2 | track(...args: unknown[]): unknown 3 | page(...args: unknown[]): unknown 4 | identify(...args: unknown[]): unknown 5 | group(...args: unknown[]): unknown 6 | alias(...args: unknown[]): unknown 7 | screen(...args: unknown[]): unknown 8 | register(...plugins: unknown[]): Promise 9 | deregister(...plugins: unknown[]): Promise 10 | readonly VERSION: string 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './emitter/interface' 2 | export * from './plugins' 3 | export * from './events/interfaces' 4 | export * from './events' 5 | export * from './callback' 6 | export * from './priority-queue' 7 | export { backoff } from './priority-queue/backoff' 8 | export * from './context' 9 | export * from './queue/event-queue' 10 | export * from './analytics' 11 | export * from './analytics/dispatch' 12 | export * from './validation/helpers' 13 | export * from './validation/errors' 14 | export * from './validation/assertions' 15 | export * from './utils/bind-all' 16 | export * from './stats' 17 | export { CoreLogger } from './logger' 18 | export * from './queue/delivery' 19 | -------------------------------------------------------------------------------- /packages/core/src/priority-queue/backoff.ts: -------------------------------------------------------------------------------- 1 | type BackoffParams = { 2 | /** The number of milliseconds before starting the first retry. Default is 500 */ 3 | minTimeout?: number 4 | 5 | /** The maximum number of milliseconds between two retries. Default is Infinity */ 6 | maxTimeout?: number 7 | 8 | /** The exponential factor to use. Default is 2. */ 9 | factor?: number 10 | 11 | /** The current attempt */ 12 | attempt: number 13 | } 14 | 15 | export function backoff(params: BackoffParams): number { 16 | const random = Math.random() + 1 17 | const { 18 | minTimeout = 500, 19 | factor = 2, 20 | attempt, 21 | maxTimeout = Infinity, 22 | } = params 23 | return Math.min(random * minTimeout * Math.pow(factor, attempt), maxTimeout) 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/task/__tests__/task-group.test.ts: -------------------------------------------------------------------------------- 1 | import { createTaskGroup } from '../task-group' 2 | 3 | function sleep(ms: number) { 4 | return new Promise((resolve) => setTimeout(resolve, ms)) 5 | } 6 | 7 | describe('TaskGroup', () => { 8 | it('works with concurrent operations', async () => { 9 | const group = createTaskGroup() 10 | const a = jest.fn() 11 | const b = jest.fn() 12 | 13 | void group.run(async () => { 14 | await sleep(100) 15 | a() 16 | }) 17 | void group.run(async () => { 18 | await sleep(1000) 19 | b() 20 | }) 21 | 22 | await group.done() 23 | expect(a).toBeCalled() 24 | expect(b).toBeCalled() 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/core/src/task/task-group.ts: -------------------------------------------------------------------------------- 1 | import { isThenable } from '../utils/is-thenable' 2 | 3 | export type TaskGroup = { 4 | done: () => Promise 5 | run: any>( 6 | op: Operation 7 | ) => ReturnType 8 | } 9 | 10 | export const createTaskGroup = (): TaskGroup => { 11 | let taskCompletionPromise: Promise 12 | let resolvePromise: () => void 13 | let count = 0 14 | 15 | return { 16 | done: () => taskCompletionPromise, 17 | run: (op) => { 18 | const returnValue = op() 19 | 20 | if (isThenable(returnValue)) { 21 | if (++count === 1) { 22 | taskCompletionPromise = new Promise((res) => (resolvePromise = res)) 23 | } 24 | 25 | returnValue.finally(() => --count === 0 && resolvePromise()) 26 | } 27 | 28 | return returnValue 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/src/user/index.ts: -------------------------------------------------------------------------------- 1 | export type ID = string | null | undefined 2 | -------------------------------------------------------------------------------- /packages/core/src/utils/bind-all.ts: -------------------------------------------------------------------------------- 1 | export function bindAll< 2 | ObjType extends { [key: string]: any }, 3 | KeyType extends keyof ObjType 4 | >(obj: ObjType): ObjType { 5 | const proto = obj.constructor.prototype 6 | for (const key of Object.getOwnPropertyNames(proto)) { 7 | if (key !== 'constructor') { 8 | const desc = Object.getOwnPropertyDescriptor( 9 | obj.constructor.prototype, 10 | key 11 | ) 12 | if (!!desc && typeof desc.value === 'function') { 13 | obj[key as KeyType] = obj[key].bind(obj) 14 | } 15 | } 16 | } 17 | 18 | return obj 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/utils/get-global.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | // This an imperfect polyfill for globalThis 3 | export const getGlobal = () => { 4 | if (typeof globalThis !== 'undefined') { 5 | return globalThis 6 | } 7 | if (typeof self !== 'undefined') { 8 | return self 9 | } 10 | if (typeof window !== 'undefined') { 11 | return window 12 | } 13 | if (typeof global !== 'undefined') { 14 | return global 15 | } 16 | return null 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/utils/group-by.ts: -------------------------------------------------------------------------------- 1 | type Grouper = (obj: T) => string | number 2 | 3 | export function groupBy( 4 | collection: T[], 5 | grouper: keyof T | Grouper 6 | ): Record { 7 | const results: Record = {} 8 | 9 | collection.forEach((item) => { 10 | let key: string | number | undefined = undefined 11 | 12 | if (typeof grouper === 'string') { 13 | const suggestedKey = item[grouper] 14 | key = 15 | typeof suggestedKey !== 'string' 16 | ? JSON.stringify(suggestedKey) 17 | : suggestedKey 18 | } else if (grouper instanceof Function) { 19 | key = grouper(item) 20 | } 21 | 22 | if (key === undefined) { 23 | return 24 | } 25 | 26 | results[key] = [...(results[key] ?? []), item] 27 | }) 28 | 29 | return results 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/utils/has-properties.ts: -------------------------------------------------------------------------------- 1 | export function hasProperties( 2 | obj: T, 3 | ...keys: K[] 4 | ): obj is T & { [J in K]: unknown } { 5 | // eslint-disable-next-line no-prototype-builtins 6 | return !!obj && keys.every((key) => obj.hasOwnProperty(key)) 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/utils/is-thenable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if thenable 3 | * (instanceof Promise doesn't respect realms) 4 | */ 5 | export const isThenable = (value: unknown): boolean => 6 | typeof value === 'object' && 7 | value !== null && 8 | 'then' in value && 9 | typeof (value as any).then === 'function' 10 | -------------------------------------------------------------------------------- /packages/core/src/utils/p-while.ts: -------------------------------------------------------------------------------- 1 | export const pWhile = async ( 2 | condition: (value: T | undefined) => boolean, 3 | action: () => T | PromiseLike 4 | ): Promise => { 5 | const loop = async (actionResult: T | undefined): Promise => { 6 | if (condition(actionResult)) { 7 | return loop(await action()) 8 | } 9 | } 10 | 11 | return loop(undefined) 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/utils/pick.ts: -------------------------------------------------------------------------------- 1 | export const pickBy = ( 2 | obj: T, 3 | fn: (key: K, v: T[K]) => boolean 4 | ) => { 5 | return (Object.keys(obj) as K[]) 6 | .filter((k) => fn(k, obj[k])) 7 | .reduce((acc, key) => ((acc[key] = obj[key]), acc), {} as Partial) 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/utils/ts-helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Remove Index Signature 3 | */ 4 | export type RemoveIndexSignature = { 5 | [K in keyof T as {} extends Record ? never : K]: T[K] 6 | } 7 | 8 | /** 9 | * Recursively make all object properties nullable 10 | */ 11 | export type DeepNullable = { 12 | [K in keyof T]: T[K] extends object ? DeepNullable | null : T[K] | null 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/validation/errors.ts: -------------------------------------------------------------------------------- 1 | export class ValidationError extends Error { 2 | field: string 3 | 4 | constructor(field: string, message: string) { 5 | super(`${field} ${message}`) 6 | this.field = field 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/validation/helpers.ts: -------------------------------------------------------------------------------- 1 | export function isString(obj: unknown): obj is string { 2 | return typeof obj === 'string' 3 | } 4 | 5 | export function isNumber(obj: unknown): obj is number { 6 | return typeof obj === 'number' 7 | } 8 | 9 | export function isFunction(obj: unknown): obj is Function { 10 | return typeof obj === 'function' 11 | } 12 | 13 | export function exists(val: unknown): val is NonNullable { 14 | return val !== undefined && val !== null 15 | } 16 | 17 | export function isPlainObject( 18 | obj: unknown 19 | ): obj is Record { 20 | return ( 21 | Object.prototype.toString.call(obj).slice(8, -1).toLowerCase() === 'object' 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/test-helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './test-ctx' 2 | export * from './test-event-queue' 3 | -------------------------------------------------------------------------------- /packages/core/test-helpers/test-ctx.ts: -------------------------------------------------------------------------------- 1 | import { CoreContext } from '../src/context' 2 | 3 | export class TestCtx extends CoreContext { 4 | static override system() { 5 | return new this({ type: 'track', event: 'system' }) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/test-helpers/test-event-queue.ts: -------------------------------------------------------------------------------- 1 | import { CoreEventQueue, PriorityQueue } from '../src' 2 | 3 | export class TestEventQueue extends CoreEventQueue { 4 | constructor() { 5 | super(new PriorityQueue(4, [])) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["**/__tests__/**", "**/*.test.*"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "outDir": "./dist/esm", 8 | "declarationDir": "./dist/types" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "exclude": ["node_modules", "dist"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "target": "ES5", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "lib": ["es2020"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/generic-utils/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../.eslintrc.isomorphic'], 4 | } 5 | -------------------------------------------------------------------------------- /packages/generic-utils/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/generic-utils/README.md: -------------------------------------------------------------------------------- 1 | # @segment/analytics-generic-utils 2 | 3 | This monorepo's version of "lodash". This package contains shared generic utilities that can be used within the ecosystem. This package should not have dependencies, and should not contain any references to the Analytics domain. 4 | -------------------------------------------------------------------------------- /packages/generic-utils/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname) 4 | -------------------------------------------------------------------------------- /packages/generic-utils/src/create-deferred/create-deferred.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return a promise that can be externally resolved 3 | */ 4 | export const createDeferred = () => { 5 | let resolve!: (value: T | PromiseLike) => void 6 | let reject!: (reason: any) => void 7 | let settled = false 8 | const promise = new Promise((_resolve, _reject) => { 9 | resolve = (...args) => { 10 | settled = true 11 | _resolve(...args) 12 | } 13 | reject = (...args) => { 14 | settled = true 15 | _reject(...args) 16 | } 17 | }) 18 | 19 | return { 20 | resolve, 21 | reject, 22 | promise, 23 | isSettled: () => settled, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/generic-utils/src/create-deferred/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-deferred' 2 | -------------------------------------------------------------------------------- /packages/generic-utils/src/emitter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './emitter' 2 | -------------------------------------------------------------------------------- /packages/generic-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-deferred' 2 | export * from './emitter' 3 | -------------------------------------------------------------------------------- /packages/generic-utils/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["**/__tests__/**", "**/*.test.*"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "outDir": "./dist/esm", 8 | "declarationDir": "./dist/types" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/generic-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "exclude": ["node_modules", "dist"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "target": "ES5", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "lib": ["es2020"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/node-integration-tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../.eslintrc'], 4 | } 5 | -------------------------------------------------------------------------------- /packages/node-integration-tests/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/node-integration-tests/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname) 4 | -------------------------------------------------------------------------------- /packages/node-integration-tests/src/cloudflare-tests/workers/README.md: -------------------------------------------------------------------------------- 1 | These workers can also be ran directly via: 2 | 3 | ``` 4 | npx wrangler dev ./src/cloudflare/workers/ 5 | ``` 6 | 7 | This can be useful if you need to debug a worker with dev tools. 8 | -------------------------------------------------------------------------------- /packages/node-integration-tests/src/cloudflare-tests/workers/forgot-close-and-flush.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Analytics } from '@segment/analytics-node' 3 | 4 | export default { 5 | fetch(_request: Request, _env: {}, _ctx: ExecutionContext) { 6 | const analytics = new Analytics({ 7 | writeKey: '__TEST__', 8 | host: 'http://localhost:3000', 9 | }) 10 | 11 | analytics.track({ userId: 'some-user', event: 'some-event' }) 12 | return new Response('ok') 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /packages/node-integration-tests/src/cloudflare-tests/workers/send-single-event.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { Analytics } from '@segment/analytics-node' 3 | 4 | export default { 5 | async fetch(_request: Request, _env: {}, _ctx: ExecutionContext) { 6 | const analytics = new Analytics({ 7 | writeKey: '__TEST__', 8 | host: 'http://localhost:3000', 9 | }) 10 | 11 | analytics.track({ userId: 'some-user', event: 'some-event' }) 12 | 13 | await analytics.closeAndFlush() 14 | return new Response('ok') 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /packages/node-integration-tests/src/durability-tests/server-start-analytics.ts: -------------------------------------------------------------------------------- 1 | import { Analytics } from '@segment/analytics-node' 2 | import { startServer } from '../server/server' 3 | import { trackEventSmall } from '../server/fixtures' 4 | 5 | const analytics = new Analytics({ 6 | writeKey: 'foo', 7 | flushInterval: 1000, 8 | flushAt: 15, 9 | }) 10 | 11 | startServer({ onClose: analytics.closeAndFlush }) 12 | .then((app) => { 13 | app.get('/', (_, res) => { 14 | analytics.track(trackEventSmall) 15 | res.sendStatus(200) 16 | }) 17 | }) 18 | .catch((err) => { 19 | console.error(err) 20 | process.exit(1) 21 | }) 22 | -------------------------------------------------------------------------------- /packages/node-integration-tests/src/perf-tests/server-start-analytics.ts: -------------------------------------------------------------------------------- 1 | import { Analytics } from '@segment/analytics-node' 2 | import { startServer } from '../server/server' 3 | import { trackEventSmall } from '../server/fixtures' 4 | 5 | const analytics = new Analytics({ 6 | writeKey: 'foo', 7 | flushInterval: 1000, 8 | flushAt: 15, 9 | }) 10 | 11 | startServer({ onClose: analytics.closeAndFlush }) 12 | .then((app) => { 13 | app.get('/', (_, res) => { 14 | analytics.track(trackEventSmall) 15 | res.sendStatus(200) 16 | }) 17 | }) 18 | .catch((err) => { 19 | console.error(err) 20 | process.exit(1) 21 | }) 22 | -------------------------------------------------------------------------------- /packages/node-integration-tests/src/perf-tests/server-start-no-analytics.ts: -------------------------------------------------------------------------------- 1 | import { startServer } from '../server/server' 2 | 3 | startServer() 4 | .then((app) => { 5 | app.get('/', (_, res) => { 6 | res.sendStatus(200) 7 | }) 8 | }) 9 | .catch((err) => { 10 | console.error(err) 11 | process.exit(1) 12 | }) 13 | -------------------------------------------------------------------------------- /packages/node-integration-tests/src/perf-tests/server-start-old-analytics.ts: -------------------------------------------------------------------------------- 1 | import Analytics from 'analytics-node' 2 | import { startServer } from '../server/server' 3 | import { trackEventSmall } from '../server/fixtures' 4 | 5 | startServer() 6 | .then((app) => { 7 | const analytics = new Analytics('foo', { flushInterval: 1000, flushAt: 15 }) 8 | app.get('/', (_, res) => { 9 | analytics.track(trackEventSmall) 10 | res.sendStatus(200) 11 | }) 12 | }) 13 | .catch((err) => { 14 | console.error(err) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/node-integration-tests/src/server/autocannon.ts: -------------------------------------------------------------------------------- 1 | import autocannon from 'autocannon' 2 | 3 | export type RunAutocannonOptions = Partial 4 | 5 | export const runAutocannon = ( 6 | options: RunAutocannonOptions = {} 7 | ): Promise => { 8 | return new Promise((resolve, reject) => { 9 | const instance = autocannon( 10 | { 11 | url: options.url || 'http://localhost:3000', 12 | ...options, 13 | }, 14 | (err, result) => { 15 | if (err) { 16 | console.error(err) 17 | reject(err) 18 | } 19 | resolve(result) 20 | } 21 | ) 22 | autocannon.track(instance, { 23 | renderProgressBar: true, 24 | renderResultsTable: false, 25 | renderLatencyTable: false, 26 | }) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /packages/node-integration-tests/src/server/fetch-polyfill.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | 3 | const majorVersion = parseInt( 4 | process.version.replace('v', '').split('.')[0], 5 | 10 6 | ) 7 | 8 | if (majorVersion >= 18) { 9 | ;(globalThis as any).fetch = fetch // polyfill fetch so nock will work on node >= 18 -- see: https://github.com/nock/nock/issues/2336 10 | } 11 | -------------------------------------------------------------------------------- /packages/node-integration-tests/src/server/fixtures.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | 3 | export const trackEventSmall = { 4 | userId: '019mr8mf4r', 5 | event: 'Order Completed', 6 | properties: { userId: 'foo', event: 'click' }, 7 | } 8 | 9 | export const trackEventLarge = { 10 | ...trackEventSmall, 11 | properties: { 12 | ...trackEventSmall.properties, 13 | data: crypto.randomBytes(1024 * 6).toString(), 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /packages/node-integration-tests/src/server/nock.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock' 2 | 3 | export const nockRequests = () => { 4 | nock.disableNetConnect() 5 | let batchEventsTotal = 0 6 | let requestTotal = 0 7 | nock('https://api.segment.io') // using regex matching in nock changes the perf profile quite a bit 8 | .post('/v1/batch', (body: any) => { 9 | requestTotal += 1 10 | const events = body.batch.length 11 | batchEventsTotal += events 12 | return true 13 | }) 14 | .reply(201) 15 | .persist() 16 | 17 | return { 18 | getRequestTotal() { 19 | return requestTotal 20 | }, 21 | getBatchEventsTotal() { 22 | return batchEventsTotal 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/node-integration-tests/src/server/types.ts: -------------------------------------------------------------------------------- 1 | export interface ServerReport { 2 | totalBatchEvents: number 3 | totalApiRequests: number 4 | averagePerBatch: number 5 | } 6 | -------------------------------------------------------------------------------- /packages/node-integration-tests/src/smoke/smoke.ts: -------------------------------------------------------------------------------- 1 | import { default as AnalyticsDefaultImport } from '@segment/analytics-node' 2 | import { Analytics as AnalyticsNamedImport } from '@segment/analytics-node' 3 | 4 | { 5 | // test named imports vs default imports 6 | new AnalyticsNamedImport({ writeKey: 'hello world' }) 7 | new AnalyticsDefaultImport({ writeKey: 'hello world' }) 8 | } 9 | -------------------------------------------------------------------------------- /packages/node-integration-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "exclude": ["node_modules", "dist"], 4 | "compilerOptions": { 5 | "resolveJsonModule": true, 6 | "module": "esnext", 7 | "target": "ES5", 8 | "moduleResolution": "node", 9 | "lib": ["es2020"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/node/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../.eslintrc.isomorphic'], 4 | rules: { 5 | '@typescript-eslint/no-empty-interface': 'off', // since this is a lib, sometimes we want to use interfaces rather than types for the ease of declaration merging. 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/node/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/node/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname, { 4 | setupFilesAfterEnv: ['./jest.setup.js'], 5 | }) 6 | -------------------------------------------------------------------------------- /packages/node/jest.setup.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | 3 | // eslint-disable-next-line no-undef 4 | globalThis.fetch = fetch // polyfill fetch so nock will work correctly on node 18 (https://github.com/nock/nock/issues/2336) 5 | -------------------------------------------------------------------------------- /packages/node/scripts/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Generate a version.ts file from the version in the package.json 3 | 4 | PKG_VERSION=$(node --eval="process.stdout.write(require('./package.json').version)") 5 | 6 | cat <src/generated/version.ts 7 | // This file is generated. 8 | export const version = '$PKG_VERSION' 9 | EOF 10 | 11 | git add src/generated/version.ts 12 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/argument-validation.integration.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestAnalytics } from './test-helpers/create-test-analytics' 2 | 3 | // This is a smoke test. 4 | // More detailed tests can be found in packages/core/src/validation/__tests__/assertions.test.ts 5 | describe('Argument validation', () => { 6 | it('should throw an error if userId/anonId/groupId is not specified', async () => { 7 | const analytics = createTestAnalytics() 8 | 9 | expect(() => 10 | analytics.track({ 11 | event: 'foo', 12 | anonymousId: undefined as any, 13 | userId: undefined as any, 14 | }) 15 | ).toThrow(/userId/) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/plugins.test.ts: -------------------------------------------------------------------------------- 1 | import { createTestAnalytics } from './test-helpers/create-test-analytics' 2 | 3 | describe('Plugins', () => { 4 | describe('Initialize', () => { 5 | it('loads analytics-node-next plugin', async () => { 6 | const analytics = createTestAnalytics() 7 | await analytics.ready 8 | 9 | const ajsNodeXt = analytics['_queue'].plugins.find( 10 | (xt) => xt.name === 'Segment.io' 11 | ) 12 | expect(ajsNodeXt).toBeDefined() 13 | expect(ajsNodeXt?.isLoaded()).toBeTruthy() 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/settings.test.ts: -------------------------------------------------------------------------------- 1 | import { validateSettings } from '../app/settings' 2 | 3 | describe('validateSettings', () => { 4 | it('should throw an error if no write key', () => { 5 | expect(() => validateSettings({ writeKey: undefined as any })).toThrowError( 6 | /writeKey/i 7 | ) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/test-helpers/assert-shape/http-request-event.ts: -------------------------------------------------------------------------------- 1 | import { NodeEmitterEvents } from '../../../app/emitter' 2 | type HttpRequestEmitterEvent = NodeEmitterEvents['http_request'][0] 3 | 4 | export const assertHttpRequestEmittedEvent = ( 5 | event: HttpRequestEmitterEvent 6 | ) => { 7 | const body = JSON.parse(event.body) 8 | expect(Array.isArray(body.batch)).toBeTruthy() 9 | expect(body.batch.length).toBe(1) 10 | expect(typeof event.headers).toBe('object') 11 | expect(typeof event.method).toBe('string') 12 | expect(typeof event.url).toBe('string') 13 | } 14 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/test-helpers/assert-shape/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-request-event' 2 | export * from './segment-http-api' 3 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/test-helpers/factories.ts: -------------------------------------------------------------------------------- 1 | export const createSuccess = (body?: any) => { 2 | return Promise.resolve({ 3 | json: () => Promise.resolve(body), 4 | text: () => Promise.resolve(JSON.stringify(body)), 5 | ok: true, 6 | status: 200, 7 | statusText: 'OK', 8 | }) as Promise 9 | } 10 | 11 | export const createError = (overrides: Partial = {}) => { 12 | return Promise.resolve({ 13 | ok: false, 14 | status: 404, 15 | statusText: 'Not Found', 16 | ...overrides, 17 | }) as Promise 18 | } 19 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/test-helpers/is-valid-date.ts: -------------------------------------------------------------------------------- 1 | export const isValidDate = (date: string) => { 2 | if (!date) { 3 | throw new Error('no date found.') 4 | } 5 | return !isNaN(Date.parse(date)) 6 | } 7 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/test-helpers/resolve-ctx.ts: -------------------------------------------------------------------------------- 1 | import type { Analytics } from '../../app/analytics-node' 2 | import type { Context } from '../../app/context' 3 | 4 | /** Tester helper that resolves context from emitter event */ 5 | export const resolveCtx = ( 6 | analytics: Analytics, 7 | eventName: 'track' | 'identify' | 'page' | 'screen' | 'group' | 'alias', 8 | { log = false } = {} 9 | ): Promise => { 10 | return new Promise((resolve, reject) => { 11 | analytics.once(eventName, resolve) 12 | analytics.once('error', (err) => { 13 | if (typeof err === 'object' && typeof err.ctx?.['logs'] === 'function') { 14 | log && console.error(err.ctx.logs()) 15 | } 16 | reject(err) 17 | }) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/test-helpers/resolve-emitter.ts: -------------------------------------------------------------------------------- 1 | import { NodeEmitter, NodeEmitterEvents } from '../../app/emitter' 2 | 3 | /** Tester helper that resolves args from emitter event */ 4 | export const resolveEmitterEvent = ( 5 | emitter: NodeEmitter, 6 | eventName: EventName 7 | ): Promise => { 8 | return new Promise((resolve) => { 9 | emitter.once(eventName, (...args) => resolve(args)) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/test-helpers/sleep.ts: -------------------------------------------------------------------------------- 1 | export function sleep(timeoutInMs: number): Promise { 2 | return new Promise((resolve) => setTimeout(resolve, timeoutInMs)) 3 | } 4 | -------------------------------------------------------------------------------- /packages/node/src/__tests__/test-helpers/test-plugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '../../app/types' 2 | 3 | export const testPlugin: Plugin = { 4 | isLoaded: jest.fn().mockReturnValue(true), 5 | load: jest.fn().mockResolvedValue(undefined), 6 | unload: jest.fn().mockResolvedValue(undefined), 7 | name: 'Test Plugin', 8 | type: 'destination', 9 | version: '0.1.0', 10 | alias: jest.fn((ctx) => Promise.resolve(ctx)), 11 | group: jest.fn((ctx) => Promise.resolve(ctx)), 12 | identify: jest.fn((ctx) => Promise.resolve(ctx)), 13 | page: jest.fn((ctx) => Promise.resolve(ctx)), 14 | screen: jest.fn((ctx) => Promise.resolve(ctx)), 15 | track: jest.fn((ctx) => Promise.resolve(ctx)), 16 | } 17 | -------------------------------------------------------------------------------- /packages/node/src/app/context.ts: -------------------------------------------------------------------------------- 1 | // create a derived class since we may want to add node specific things to Context later 2 | 3 | import { CoreContext } from '@segment/analytics-core' 4 | import { SegmentEvent } from './types' 5 | 6 | // While this is not a type, it is a definition 7 | export class Context extends CoreContext { 8 | static override system() { 9 | return new this({ type: 'track', event: 'system' }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/node/src/app/emitter.ts: -------------------------------------------------------------------------------- 1 | import type { CoreEmitterContract } from '@segment/analytics-core' 2 | import { Emitter } from '@segment/analytics-generic-utils' 3 | import { Context } from './context' 4 | import type { AnalyticsSettings } from './settings' 5 | import { SegmentEvent } from './types' 6 | 7 | /** 8 | * Map of emitter event names to method args. 9 | */ 10 | export type NodeEmitterEvents = CoreEmitterContract & { 11 | initialize: [AnalyticsSettings] 12 | call_after_close: [SegmentEvent] // any event that did not get dispatched due to close 13 | http_request: [ 14 | { 15 | url: string 16 | method: string 17 | headers: Record 18 | body: string 19 | } 20 | ] 21 | drained: [] 22 | } 23 | 24 | export class NodeEmitter extends Emitter {} 25 | -------------------------------------------------------------------------------- /packages/node/src/app/event-queue.ts: -------------------------------------------------------------------------------- 1 | import { CoreEventQueue, PriorityQueue } from '@segment/analytics-core' 2 | import type { Plugin } from '../app/types' 3 | import type { Context } from './context' 4 | 5 | class NodePriorityQueue extends PriorityQueue { 6 | constructor() { 7 | super(1, []) 8 | } 9 | // do not use an internal "seen" map 10 | getAttempts(ctx: Context): number { 11 | return ctx.attempts ?? 0 12 | } 13 | updateAttempts(ctx: Context): number { 14 | ctx.attempts = this.getAttempts(ctx) + 1 15 | return this.getAttempts(ctx) 16 | } 17 | } 18 | 19 | export class NodeEventQueue extends CoreEventQueue { 20 | constructor() { 21 | super(new NodePriorityQueue()) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/node/src/app/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './params' 2 | export * from './segment-event' 3 | export * from './plugin' 4 | -------------------------------------------------------------------------------- /packages/node/src/app/types/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { CorePlugin } from '@segment/analytics-core' 2 | import type { Analytics } from '../analytics-node' 3 | import type { Context } from '../context' 4 | 5 | export interface Plugin extends CorePlugin {} 6 | -------------------------------------------------------------------------------- /packages/node/src/app/types/segment-event.ts: -------------------------------------------------------------------------------- 1 | import type { CoreSegmentEvent } from '@segment/analytics-core' 2 | 3 | type SegmentEventType = 'track' | 'page' | 'identify' | 'alias' | 'screen' 4 | 5 | export interface SegmentEvent extends CoreSegmentEvent { 6 | type: SegmentEventType 7 | } 8 | -------------------------------------------------------------------------------- /packages/node/src/generated/version.ts: -------------------------------------------------------------------------------- 1 | // This file is generated. 2 | export const version = '2.2.1' 3 | -------------------------------------------------------------------------------- /packages/node/src/index.common.ts: -------------------------------------------------------------------------------- 1 | export { Analytics } from './app/analytics-node' 2 | export { Context } from './app/context' 3 | export { 4 | HTTPClient, 5 | FetchHTTPClient, 6 | HTTPFetchRequest, 7 | HTTPResponse, 8 | HTTPFetchFn, 9 | HTTPClientRequest, 10 | } from './lib/http-client' 11 | 12 | export { OAuthSettings } from './lib/types' 13 | 14 | export type { 15 | Plugin, 16 | GroupTraits, 17 | UserTraits, 18 | TrackParams, 19 | IdentifyParams, 20 | AliasParams, 21 | GroupParams, 22 | PageParams, 23 | } from './app/types' 24 | export type { AnalyticsSettings } from './app/settings' 25 | -------------------------------------------------------------------------------- /packages/node/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './index.common' 2 | 3 | // export Analytics as both a named export and a default export (for backwards-compat. reasons) 4 | import { Analytics } from './index.common' 5 | export default Analytics 6 | -------------------------------------------------------------------------------- /packages/node/src/lib/__tests__/get-message-id.test.ts: -------------------------------------------------------------------------------- 1 | import { createMessageId } from '../get-message-id' 2 | 3 | // https://gist.github.com/johnelliott/cf77003f72f889abbc3f32785fa3df8d 4 | const uuidv4Regex = 5 | /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i 6 | 7 | describe(createMessageId, () => { 8 | it('returns a string in the format "node-next-[unix epoch time]-[uuid v4]"', () => { 9 | const msg = createMessageId().split('-') 10 | expect(msg.length).toBe(8) 11 | 12 | expect(`${msg[0]}-${msg[1]}`).toBe('node-next') 13 | 14 | const epochTimeSeg = msg[2] 15 | expect(typeof parseInt(epochTimeSeg)).toBe('number') 16 | expect(epochTimeSeg.length > 10).toBeTruthy() 17 | 18 | const uuidSeg = msg.slice(3).join('-') 19 | expect(uuidSeg).toEqual(expect.stringMatching(uuidv4Regex)) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /packages/node/src/lib/base-64-encode.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-nodejs-modules 2 | import { Buffer } from 'buffer' 3 | /** 4 | * Base64 encoder that works in browser, worker, node runtimes. 5 | */ 6 | export const b64encode = (str: string): string => { 7 | return Buffer.from(str).toString('base64') 8 | } 9 | -------------------------------------------------------------------------------- /packages/node/src/lib/create-url.ts: -------------------------------------------------------------------------------- 1 | const stripTrailingSlash = (str: string) => str.replace(/\/$/, '') 2 | 3 | /** 4 | * 5 | * @param host e.g. "http://foo.com" 6 | * @param path e.g. "/bar" 7 | * @returns "e.g." "http://foo.com/bar" 8 | */ 9 | export const tryCreateFormattedUrl = (host: string, path?: string) => { 10 | return stripTrailingSlash(new URL(path || '', host).href) 11 | } 12 | -------------------------------------------------------------------------------- /packages/node/src/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import type { HTTPFetchFn } from './http-client' 2 | 3 | export const fetch: HTTPFetchFn = async (...args) => { 4 | if (globalThis.fetch) { 5 | return globalThis.fetch(...args) 6 | } 7 | // This guard causes is important, as it causes dead-code elimination to be enabled inside this block. 8 | // @ts-ignore 9 | else if (typeof EdgeRuntime !== 'string') { 10 | return (await import('node-fetch')).default(...args) 11 | } else { 12 | throw new Error( 13 | 'Invariant: an edge runtime that does not support fetch should not exist' 14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/node/src/lib/get-message-id.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from './uuid' 2 | 3 | /** 4 | * get a unique messageId with a very low chance of collisions 5 | * using @lukeed/uuid/secure uses the node crypto module, which is the fastest 6 | * @example "node-next-1668208232027-743be593-7789-4b74-8078-cbcc8894c586" 7 | */ 8 | export const createMessageId = (): string => { 9 | return `node-next-${Date.now()}-${uuid()}` 10 | } 11 | -------------------------------------------------------------------------------- /packages/node/src/lib/uuid.ts: -------------------------------------------------------------------------------- 1 | export { v4 as uuid } from '@lukeed/uuid' 2 | -------------------------------------------------------------------------------- /packages/node/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["**/__tests__/**"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "outDir": "./dist/esm", 8 | "declarationDir": "./dist/types" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "exclude": ["node_modules", "dist"], 4 | "compilerOptions": { 5 | "module": "ESNext", 6 | "target": "es2022", // node 18 7 | "moduleResolution": "node", 8 | "lib": ["es2022"] // TODO: es2023 https://www.npmjs.com/package/@tsconfig/node18 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/page-tools/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../.eslintrc'], 4 | } 5 | -------------------------------------------------------------------------------- /packages/page-tools/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/page-tools/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @segment/analytics-page-tools 2 | 3 | ## 1.0.0 4 | 5 | ### Major Changes 6 | 7 | - [#1271](https://github.com/segmentio/analytics-next/pull/1271) [`f2c2b764`](https://github.com/segmentio/analytics-next/commit/f2c2b764c168b954218f1fedce19c1aabfb5d26d) Thanks [@silesky](https://github.com/silesky)! - Release package 8 | -------------------------------------------------------------------------------- /packages/page-tools/README.md: -------------------------------------------------------------------------------- 1 | # @segment/analytics-page-tools 2 | 3 | Browser-only helpers for generating page events with analytics.js. 4 | -------------------------------------------------------------------------------- /packages/page-tools/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname, { 4 | testEnvironment: 'jsdom', 5 | }) 6 | -------------------------------------------------------------------------------- /packages/page-tools/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | BufferedPageContextDiscriminant, 3 | createBufferedPageContext, 4 | createPageContext, 5 | getDefaultBufferedPageContext, 6 | PageContext, 7 | BufferedPageContext, 8 | getDefaultPageContext, 9 | isBufferedPageContext, 10 | } from './page-context' 11 | -------------------------------------------------------------------------------- /packages/page-tools/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["**/__tests__/**"], 5 | "compilerOptions": { 6 | "noEmit": false 7 | // Options tsup Ignores: 8 | // module: tsup always outputs ESM or CommonJS based on its --format option. 9 | // outDir: tsup uses its own --out-dir option to control output. 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/page-tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "exclude": ["node_modules", "dist"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "target": "ES5", 7 | "moduleResolution": "node", 8 | "lib": ["es2020", "DOM", "DOM.Iterable"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/page-tools/tsup.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@internal/config-tsup/config') 2 | -------------------------------------------------------------------------------- /packages/signals/signals-example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ] 7 | } -------------------------------------------------------------------------------- /packages/signals/signals-example/.env.example: -------------------------------------------------------------------------------- 1 | # Configure + rename this file to .env 2 | 3 | WRITEKEY=your-write-key 4 | STAGE=false -------------------------------------------------------------------------------- /packages/signals/signals-example/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../../.eslintrc'], 4 | env: { 5 | browser: true, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/signals/signals-example/README.md: -------------------------------------------------------------------------------- 1 | # Signals Example 2 | 3 | ### Instructions 4 | - `cd packages/signals/signal-example` 5 | - `yarn install` 6 | - `yarn . build` 7 | - Rename: `.env.example` -> `.env` (set `WRITEKEY=XXX` in `.env`) 8 | - `yarn dev` 9 | - 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. -------------------------------------------------------------------------------- /packages/signals/signals-example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './components/App' 4 | import './styles.css' 5 | 6 | ReactDOM.render(, document.getElementById('root')) 7 | -------------------------------------------------------------------------------- /packages/signals/signals-example/src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ComplexForm from '../components/ComplexForm' 3 | import { analytics } from '../lib/analytics' 4 | 5 | export const HomePage: React.FC = () => { 6 | return ( 7 |
8 |

Hello, React with TypeScript!

9 | 10 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/signals/signals-example/src/pages/Other.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const OtherPage: React.FC = () => { 4 | React.useEffect(() => { 5 | document.title = 'Other Page' 6 | }, []) 7 | 8 | return ( 9 |
10 |

Hello, I'm another page

11 | Go to Section 12 |
13 |

A section

14 |

{`Lorem ipsum dolor sit amet`}

15 |
16 | Go to Top 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/signals/signals-example/src/pages/ReactHookForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ComplexForm from '../components/ComplexReactHookForm' 3 | import { analytics } from '../lib/analytics' 4 | 5 | export const ReactHookFormPage: React.FC = () => { 6 | return ( 7 |
8 |

Hello, React Hook Form with TypeScript!

9 | 10 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/signals/signals-example/src/styles.css: -------------------------------------------------------------------------------- 1 | main { 2 | width: 960px; 3 | margin: auto; 4 | } 5 | 6 | form { 7 | display: flex; 8 | flex-direction: column; 9 | max-width: 300px; 10 | margin: 0 auto; 11 | } 12 | 13 | label { 14 | margin-bottom: 10px; 15 | } 16 | 17 | input, 18 | select { 19 | width: 100%; 20 | padding: 10px; 21 | margin-top: 5px; 22 | } 23 | 24 | button { 25 | padding: 10px; 26 | cursor: pointer; 27 | } 28 | nav { 29 | background-color: #f8f9fa; 30 | padding: 10px 0; 31 | margin-bottom: px; 32 | } 33 | 34 | nav a { 35 | margin: 0 10px; 36 | text-decoration: none; 37 | color: #333; 38 | } 39 | 40 | nav a:hover { 41 | color: #007bff; 42 | } 43 | 44 | form div { 45 | display: inline-block; 46 | } -------------------------------------------------------------------------------- /packages/signals/signals-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES6", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "outDir": "./dist" 12 | }, 13 | "include": ["src"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ] 7 | } -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../../.eslintrc'], 4 | env: { 5 | browser: true, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname) 4 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/playwright.global-setup.ts: -------------------------------------------------------------------------------- 1 | import type { FullConfig } from '@playwright/test' 2 | import { execSync } from 'child_process' 3 | import { envConfig } from './src/helpers/env-config' 4 | 5 | export default function globalSetup(_cfg: FullConfig) { 6 | console.log(`Executing playwright.global-setup.ts...\n`) 7 | console.log(`Using envConfig: ${JSON.stringify(envConfig, undefined, 2)}\n`) 8 | if (process.env.SKIP_BUILD !== 'true') { 9 | console.log(`Executing yarn build:\n`) 10 | execSync('yarn build', { stdio: 'inherit' }) 11 | } 12 | console.log('Finished global setup. Should start running tests.\n') 13 | } 14 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/helpers/env-config.ts: -------------------------------------------------------------------------------- 1 | import { SignalsPluginSettingsConfig } from '@segment/analytics-signals' 2 | 3 | // This is for testing with the global sandbox strategy with an npm script, that executes processSignal in the global scope 4 | // If we change this to be the default, this can be rejiggered 5 | const SANDBOX_STRATEGY = (process.env.SANDBOX_STRATEGY ?? 6 | 'iframe') as SignalsPluginSettingsConfig['sandboxStrategy'] 7 | 8 | export const envConfig = { 9 | SANDBOX_STRATEGY, 10 | } 11 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/helpers/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { PageData } from '@segment/analytics-signals-runtime' 2 | 3 | const pageData: PageData = { 4 | hash: '', 5 | hostname: 'localhost', 6 | path: '/src/tests/signals-vanilla/index.html', 7 | referrer: '', 8 | search: '', 9 | title: '', 10 | url: 'http://localhost:5432/src/tests/signals-vanilla/index.html', 11 | } 12 | 13 | export const commonSignalData = { 14 | page: pageData, 15 | } 16 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/helpers/log-console.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@playwright/test' 2 | 3 | export const logConsole = (page: Page) => { 4 | page.on('console', (msg) => { 5 | const text = msg.text() 6 | // keep stdout clean, e.g. by not printing intentional errors 7 | const ignoreList = ['Bad Request'] 8 | if (ignoreList.some((str) => text.includes(str))) { 9 | return 10 | } 11 | console.log(`console.${msg.type()}:`, text) 12 | }) 13 | page.on('pageerror', (error) => { 14 | console.error('Page error:', error) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/helpers/ts.ts: -------------------------------------------------------------------------------- 1 | export type Compute = { [K in keyof T]: T[K] } & {} 2 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/shims.d.ts: -------------------------------------------------------------------------------- 1 | import type { AnalyticsBrowser } from '@segment/analytics-next' 2 | import type { SignalsPlugin } from '@segment/analytics-signals' 3 | 4 | declare global { 5 | interface Window { 6 | analytics: AnalyticsBrowser 7 | signalsPlugin: SignalsPlugin 8 | SignalsPlugin: typeof SignalsPlugin 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TextField } from './TextField' 3 | import { Select, SelectItem } from './Select' 4 | 5 | export const App: React.FC = () => { 6 | return ( 7 |
8 |
9 |

TextField

10 | 11 |
12 |
13 |

Select

14 | 20 |
21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button as RACButton, ButtonProps } from 'react-aria-components' 3 | import './Button.css' 4 | 5 | export function Button(props: ButtonProps) { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.css: -------------------------------------------------------------------------------- 1 | @import "./theme.css"; 2 | @import './Button.css'; 3 | @import './TextField.css'; 4 | @import './Modal.css'; 5 | 6 | .react-aria-Dialog { 7 | outline: none; 8 | padding: 30px; 9 | max-height: inherit; 10 | box-sizing: border-box; 11 | overflow: auto; 12 | 13 | .react-aria-Heading[slot=title] { 14 | line-height: 1em; 15 | margin-top: 0; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/components/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog as RACDialog, DialogProps } from 'react-aria-components' 2 | import './Dialog.css' 3 | import React from 'react' 4 | 5 | export function Dialog(props: DialogProps) { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.css: -------------------------------------------------------------------------------- 1 | @import "./theme.css"; 2 | @import './TextField.css'; 3 | @import './Button.css'; 4 | 5 | .react-aria-Form { 6 | display: flex; 7 | flex-direction: column; 8 | align-items: start; 9 | gap: 8px; 10 | } 11 | 12 | .react-aria-Form [role=alert] { 13 | border: 2px solid var(--invalid-color); 14 | background: var(--overlay-background); 15 | border-radius: 6px; 16 | padding: 12px; 17 | max-width: 250px; 18 | outline: none; 19 | 20 | &:focus-visible { 21 | outline: 2px solid var(--focus-ring-color); 22 | outline-offset: 2px; 23 | } 24 | 25 | h3 { 26 | margin-top: 0; 27 | } 28 | 29 | p { 30 | margin-bottom: 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/components/Form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Form as RACForm, FormProps } from 'react-aria-components' 3 | import './Form.css' 4 | 5 | export function Form(props: FormProps) { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/components/ListBox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | ListBox as AriaListBox, 4 | ListBoxItem as AriaListBoxItem, 5 | ListBoxItemProps, 6 | ListBoxProps, 7 | } from 'react-aria-components' 8 | 9 | import './ListBox.css' 10 | 11 | export function ListBox({ 12 | children, 13 | ...props 14 | }: ListBoxProps) { 15 | return {children} 16 | } 17 | 18 | export function ListBoxItem(props: ListBoxItemProps) { 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { Modal as RACModal, ModalOverlayProps } from 'react-aria-components' 2 | import './Modal.css' 3 | import React from 'react' 4 | 5 | export function Modal(props: ModalOverlayProps) { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/components/Popover.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Dialog, 4 | OverlayArrow, 5 | Popover as AriaPopover, 6 | PopoverProps as AriaPopoverProps, 7 | } from 'react-aria-components' 8 | 9 | import './Popover.css' 10 | 11 | export interface PopoverProps extends Omit { 12 | children: React.ReactNode 13 | } 14 | 15 | export function Popover({ children, ...props }: PopoverProps) { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | {children} 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/components/Switch.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Switch as AriaSwitch, 3 | SwitchProps as AriaSwitchProps, 4 | } from 'react-aria-components' 5 | import React from 'react' 6 | import './Switch.css' 7 | 8 | export interface SwitchProps extends Omit { 9 | children: React.ReactNode 10 | } 11 | 12 | export function Switch({ children, ...props }: SwitchProps) { 13 | return ( 14 | 15 |
16 | {children} 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tabs as RACTabs, TabsProps } from 'react-aria-components' 3 | import './Tabs.css' 4 | 5 | export function Tabs(props: TabsProps) { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/index-page.ts: -------------------------------------------------------------------------------- 1 | import { BasePage } from '../../helpers/base-page-object' 2 | 3 | export class IndexPage extends BasePage { 4 | constructor() { 5 | super(`/custom-elements/index.html`) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/index.bundle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { AnalyticsBrowser } from '@segment/analytics-next' 4 | import { SignalsPlugin } from '@segment/analytics-signals' 5 | import { App } from './components/App' 6 | 7 | window.SignalsPlugin = SignalsPlugin 8 | window.analytics = new AnalyticsBrowser() 9 | 10 | const container = document.getElementById('root') 11 | const root = createRoot(container!) 12 | root.render() 13 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/custom-elements/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | > 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/performance/index-page.ts: -------------------------------------------------------------------------------- 1 | import { BasePage } from '../../helpers/base-page-object' 2 | 3 | export class IndexPage extends BasePage { 4 | constructor() { 5 | super(`/performance/index.html`) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/performance/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.bundle.ts: -------------------------------------------------------------------------------- 1 | import { AnalyticsBrowser } from '@segment/analytics-next' 2 | import { SignalsPlugin } from '@segment/analytics-signals' 3 | 4 | /** 5 | * Not calling analytics.load() or instantiating Signals Plugin here, as all this configuration happens in the page object. 6 | */ 7 | window.SignalsPlugin = SignalsPlugin 8 | window.analytics = new AnalyticsBrowser() 9 | -------------------------------------------------------------------------------- /packages/signals/signals-integration-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "exclude": ["node_modules", "dist", "playwright-report"], 4 | "compilerOptions": { 5 | "jsx": "react", 6 | "module": "esnext", 7 | "target": "ES2022", 8 | "moduleResolution": "node", 9 | "lib": ["es2020"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../../.eslintrc'], 4 | env: { 5 | browser: true, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/api-extractor.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | "compiler": { 4 | "tsconfigFilePath": "./tsconfig.build.json" 5 | }, 6 | "messages": { 7 | "compilerMessageReporting": { 8 | "default": { 9 | "logLevel": "none" 10 | } 11 | }, 12 | "extractorMessageReporting": { 13 | "default": { 14 | "logLevel": "none" 15 | }, 16 | "ae-missing-release-tag": { 17 | "logLevel": "none" 18 | }, 19 | "ae-internal-missing-underscore": { 20 | "logLevel": "none" 21 | }, 22 | "ae-forgotten-export": { 23 | "logLevel": "none" 24 | } 25 | } 26 | }, 27 | "apiReport": { 28 | "enabled": false 29 | }, 30 | "docModel": { 31 | "enabled": false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/api-extractor.mobile.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./api-extractor.base.json", 3 | "mainEntryPointFilePath": "./dist/types/mobile/index.mobile-editor.d.ts", 4 | "dtsRollup": { 5 | "enabled": true, 6 | "untrimmedFilePath": "./dist/editor/mobile-editor.d.ts.txt" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/api-extractor.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./api-extractor.base.json", 3 | "mainEntryPointFilePath": "./dist/types/web/index.web-editor.d.ts", 4 | "dtsRollup": { 5 | "enabled": true, 6 | "untrimmedFilePath": "./dist/editor/web-editor.d.ts.txt" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname, { 4 | testEnvironment: 'jsdom', 5 | setupFilesAfterEnv: ['./jest.setup.js'], 6 | }) 7 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/jest.setup.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch') 2 | const { TextEncoder, TextDecoder } = require('util') 3 | 4 | // fix: "ReferenceError: TextEncoder is not defined" after upgrading JSDOM 5 | global.TextEncoder = TextEncoder 6 | global.TextDecoder = TextDecoder 7 | 8 | // eslint-disable-next-line no-undef 9 | globalThis.fetch = fetch // polyfill fetch so nock will work correctly on node 18 (https://github.com/nock/nock/issues/2336) 10 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/scripts/assert-generated.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # A CI script to ensure people remember to rebuild workerbox related files if workerbox changes 3 | 4 | yarn build:global 5 | 6 | # Check for changes in the workerbox directory 7 | changed_files=$(git diff --name-only | grep 'generated') 8 | 9 | # Check for changes in the workerbox directory 10 | if [ -n "$changed_files" ]; then 11 | echo "Error: Changes detected. Please commit the changed files:" 12 | echo "$changed_files" 13 | exit 1 14 | else 15 | echo "Files have not changed" 16 | exit 0 17 | fi 18 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/src/index.ts: -------------------------------------------------------------------------------- 1 | // This file is only for people who plan on installing this package in their npm projects, like us. 2 | 3 | // shared 4 | export { SignalsRuntime } from './shared/signals-runtime' 5 | 6 | // web 7 | export * from './web/web-signals-types' 8 | export * from './shared/shared-types' 9 | export * as WebRuntimeConstants from './web/web-constants' 10 | export { getRuntimeCode } from './web/get-runtime-code.generated' 11 | export { WebSignalsRuntime } from './web/web-signals-runtime' 12 | 13 | // mobile -- we don't need this *yet*, but some day? 14 | export * as Mobile from './mobile/mobile-signals-types' 15 | export * as MobileRuntimeConstants from './mobile/mobile-constants' 16 | export { MobileSignalsRuntime } from './mobile/mobile-signals-runtime' 17 | export { getRuntimeCode as getMobileRuntimeCode } from './mobile/get-runtime-code.generated' 18 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/src/mobile/index.mobile-editor.ts: -------------------------------------------------------------------------------- 1 | import { MobileSignalsRuntime } from './mobile-signals-runtime' 2 | export { MobileSignalsRuntime } 3 | 4 | export const signals = new MobileSignalsRuntime() 5 | 6 | /** 7 | * Entry point for the editor definitions 8 | */ 9 | export * from './mobile-signals-types' 10 | export * from '../shared/shared-types' 11 | export * from './mobile-constants' 12 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/src/mobile/index.signals-runtime.ts: -------------------------------------------------------------------------------- 1 | import * as Constants from './mobile-constants' 2 | import { MobileSignalsRuntime } from './mobile-signals-runtime' 3 | 4 | // assign SignalsRuntime and all constants to globalThis 5 | // meant to replace this: 6 | // https://github.com/segmentio/SignalsJS-Runtime/blob/main/Runtime/Signals.js 7 | Object.assign( 8 | globalThis, 9 | { 10 | signals: new MobileSignalsRuntime(), 11 | }, 12 | Constants 13 | ) 14 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/src/shared/shared-types.ts: -------------------------------------------------------------------------------- 1 | export type ID = string | null | undefined 2 | 3 | export interface BaseSignal { 4 | type: string 5 | anonymousId: ID 6 | timestamp: string 7 | } 8 | 9 | export type SignalOfType< 10 | AllSignals extends BaseSignal, 11 | SignalType extends AllSignals['type'] 12 | > = AllSignals & { type: SignalType } 13 | 14 | export type JSONPrimitive = string | number | boolean | null 15 | export type JSONValue = JSONPrimitive | JSONObject | JSONArray 16 | export type JSONObject = { [member: string]: JSONValue } 17 | export type JSONArray = JSONValue[] 18 | 19 | export interface SegmentEvent { 20 | /** 21 | * @example 'track' | 'page' | 'screen' | 'identify' | 'group' | 'alias' 22 | */ 23 | type: string 24 | [key: string]: unknown 25 | } 26 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/src/web/index.signals-runtime.ts: -------------------------------------------------------------------------------- 1 | import { WebSignalsRuntime } from './web-signals-runtime' 2 | import * as Constants from './web-constants' 3 | 4 | // assign SignalsRuntime and all constants to globalThis 5 | Object.assign( 6 | globalThis, 7 | { 8 | signals: new WebSignalsRuntime(), 9 | }, 10 | Constants 11 | ) 12 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/src/web/index.web-editor.ts: -------------------------------------------------------------------------------- 1 | import { WebSignalsRuntime } from './web-signals-runtime' 2 | export { WebSignalsRuntime } 3 | 4 | export const signals = new WebSignalsRuntime() 5 | 6 | /** 7 | * Entry point for the editor definitions 8 | */ 9 | export * from './web-signals-types' 10 | export * from '../shared/shared-types' 11 | export * from './web-constants' 12 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/src/web/web-constants.ts: -------------------------------------------------------------------------------- 1 | export const EventType = Object.freeze({ 2 | Track: 'track', 3 | Page: 'page', 4 | Screen: 'screen', 5 | Identify: 'identify', 6 | Group: 'group', 7 | Alias: 'alias', 8 | }) 9 | 10 | export const NavigationAction = Object.freeze({ 11 | URLChange: 'urlChange', 12 | PageLoad: 'pageLoad', 13 | }) 14 | 15 | export const SignalType = Object.freeze({ 16 | Interaction: 'interaction', 17 | Navigation: 'navigation', 18 | Network: 'network', 19 | LocalData: 'localData', 20 | Instrumentation: 'instrumentation', 21 | UserDefined: 'userDefined', 22 | }) 23 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/src/web/web-signals-runtime.ts: -------------------------------------------------------------------------------- 1 | import { SignalsRuntime } from '../shared/signals-runtime' 2 | import { Signal } from './web-signals-types' 3 | 4 | export class WebSignalsRuntime extends SignalsRuntime {} 5 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["**/__tests__/**", "**/test-helpers/**"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "outDir": "./dist/esm", 8 | "declarationDir": "./dist/types" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/signals/signals-runtime/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "exclude": ["node_modules", "dist", "src/generated"], 4 | "compilerOptions": { 5 | "module": "ESNext", // es6 modules 6 | "target": "ES2020", // don't down-compile *too much* -- if users are using webpack, they can always transpile this library themselves 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], // assume that consumers will be polyfilling at least down to es2020 8 | "moduleResolution": "node", 9 | 10 | "isolatedModules": true // ensure we are friendly to build systems 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/signals/signals/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../../.eslintrc'], 4 | env: { 5 | browser: true, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/signals/signals/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("@internal/config").lintStagedConfig, 3 | 'src/lib/workerbox/*.{js,ts,html}': ['yarn workerbox'] 4 | } 5 | 6 | -------------------------------------------------------------------------------- /packages/signals/signals/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname, { 4 | testEnvironment: 'jsdom', 5 | setupFilesAfterEnv: ['./jest.setup.ts'], 6 | }) 7 | -------------------------------------------------------------------------------- /packages/signals/signals/jest.setup.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import './src/test-helpers/jest-extended' 3 | import { JestSerializers } from '@internal/test-helpers' 4 | import 'fake-indexeddb/auto' 5 | globalThis.structuredClone = (v: any) => JSON.parse(JSON.stringify(v)) 6 | expect.addSnapshotSerializer(JestSerializers.jestSnapshotSerializerTimestamp) 7 | -------------------------------------------------------------------------------- /packages/signals/signals/scripts/assert-workerbox-built.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # A CI script to ensure people remember to rebuild workerbox related files if workerbox changes 3 | 4 | node scripts/build-workerbox.js 5 | # Check for changes in the workerbox directory 6 | changed_files=$(git diff --name-only | grep 'lib/workerbox') 7 | 8 | # Check for changes in the workerbox directory 9 | if [ -n "$changed_files" ]; then 10 | echo "Error: Changes detected in the workerbox directory. Please commit the changed files:" 11 | echo "$changed_files" 12 | exit 1 13 | else 14 | echo "Files have not changed" 15 | exit 0 16 | fi -------------------------------------------------------------------------------- /packages/signals/signals/scripts/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Generate a version.ts file from the version in the package.json 3 | 4 | PKG_VERSION=$(node --eval="process.stdout.write(require('./package.json').version)") 5 | 6 | cat <src/generated/version.ts 7 | // This file is generated. 8 | export const version = '$PKG_VERSION' 9 | EOF 10 | 11 | git add src/generated/version.ts 12 | -------------------------------------------------------------------------------- /packages/signals/signals/src/core/middleware/signals-ingest/index.ts: -------------------------------------------------------------------------------- 1 | import { Signal } from '@segment/analytics-signals-runtime' 2 | import { SignalsSubscriber, SignalsMiddlewareContext } from '../../emitter' 3 | import { SignalsIngestClient } from './signals-ingest-client' 4 | 5 | export class SignalsIngestSubscriber implements SignalsSubscriber { 6 | client!: SignalsIngestClient 7 | ctx!: SignalsMiddlewareContext 8 | load(ctx: SignalsMiddlewareContext) { 9 | this.ctx = ctx 10 | this.client = new SignalsIngestClient( 11 | ctx.analyticsInstance.settings.writeKey, 12 | ctx.unstableGlobalSettings.ingestClient 13 | ) 14 | } 15 | process(signal: Signal) { 16 | void this.client.send(signal) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/signals/signals/src/core/middleware/user-info/index.ts: -------------------------------------------------------------------------------- 1 | import { Signal } from '@segment/analytics-signals-runtime' 2 | import { UserInfo } from '../../../types' 3 | import { SignalsMiddleware, SignalsMiddlewareContext } from '../../emitter' 4 | 5 | export class UserInfoMiddleware implements SignalsMiddleware { 6 | user!: UserInfo 7 | 8 | load(ctx: SignalsMiddlewareContext) { 9 | this.user = ctx.analyticsInstance.user() 10 | } 11 | 12 | process(signal: Signal): Signal { 13 | // anonymousId should always exist here unless the user is explicitly setting it to null 14 | signal.anonymousId = this.user.anonymousId() 15 | return signal 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/signals/signals/src/core/processor/arg-resolvers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | resolveAliasArguments, 3 | resolveArguments, 4 | resolvePageArguments, 5 | resolveUserArguments, 6 | } from '@segment/analytics-next' 7 | 8 | export const resolvers = { 9 | resolveAliasArguments, 10 | resolveArguments, 11 | resolvePageArguments, 12 | resolveUserArguments: resolveUserArguments({ 13 | id: () => undefined, 14 | }), 15 | } 16 | -------------------------------------------------------------------------------- /packages/signals/signals/src/core/processor/polyfills.ts: -------------------------------------------------------------------------------- 1 | const globalThisPolyfill = `(function () { 2 | // polyfill for globalThis 3 | if (typeof globalThis === 'undefined') { 4 | if (typeof self !== 'undefined') { 5 | self.globalThis = self 6 | } else if (typeof window !== 'undefined') { 7 | window.globalThis = window 8 | } else if (typeof global !== 'undefined') { 9 | global.globalThis = global 10 | } else { 11 | throw new Error('Unable to locate global object') 12 | } 13 | } 14 | })()` 15 | 16 | export const polyfills = [globalThisPolyfill].join('\n') 17 | -------------------------------------------------------------------------------- /packages/signals/signals/src/core/signal-generators/dom-gen/helpers.ts: -------------------------------------------------------------------------------- 1 | export const cleanText = (str: string): string => { 2 | return str 3 | .replace(/[\r\n\t]+/g, ' ') // Replace newlines and tabs with a space 4 | .replace(/\s\s+/g, ' ') // Replace multiple spaces with a single space 5 | .replace(/\u00A0/g, ' ') // Replace non-breaking spaces with a regular space 6 | .trim() // Trim leading and trailing spaces 7 | } 8 | 9 | // Check if a subset object is a partial match of another object 10 | export const isObjectMatch = >( 11 | partialObj: Partial, 12 | mainObj: Obj 13 | ): boolean => { 14 | return Object.keys(partialObj).every( 15 | (key) => partialObj[key as keyof Obj] === mainObj[key as keyof Obj] 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/signals/signals/src/core/signal-generators/dom-gen/index.ts: -------------------------------------------------------------------------------- 1 | import { ClickSignalsGenerator, FormSubmitGenerator } from './dom-gen' 2 | import { 3 | MutationChangeGenerator, 4 | OnChangeGenerator, 5 | ContentEditableChangeGenerator, 6 | } from './change-gen' 7 | import { SignalGeneratorClass } from '../types' 8 | import { OnNavigationEventGenerator } from './navigation-gen' 9 | 10 | export const domGenerators: SignalGeneratorClass[] = [ 11 | MutationChangeGenerator, 12 | OnChangeGenerator, 13 | ContentEditableChangeGenerator, 14 | ClickSignalsGenerator, 15 | FormSubmitGenerator, 16 | OnNavigationEventGenerator, 17 | ] 18 | -------------------------------------------------------------------------------- /packages/signals/signals/src/core/signal-generators/types.ts: -------------------------------------------------------------------------------- 1 | import type { SignalEmitter } from '../emitter' 2 | import { SignalGlobalSettings } from '../signals' 3 | 4 | export interface SignalGenerator { 5 | /** 6 | * To support unregistering by name/label 7 | * e.g "form-submit" 8 | */ 9 | id?: string 10 | /** 11 | * Register a custom function that emits signals. 12 | * If this function returns a promise, signals client will not be able to send signals until the promise resolves. 13 | */ 14 | register(emitter: SignalEmitter): (() => void) | Promise<() => void> 15 | } 16 | 17 | export interface SignalGeneratorClass { 18 | id?: string 19 | new (settings: SignalGlobalSettings): SignalGenerator 20 | } 21 | -------------------------------------------------------------------------------- /packages/signals/signals/src/core/signals/index.ts: -------------------------------------------------------------------------------- 1 | // This is a barrel file, and should only contain exports. 2 | export * from './signals' 3 | export * from './settings' 4 | -------------------------------------------------------------------------------- /packages/signals/signals/src/generated/version.ts: -------------------------------------------------------------------------------- 1 | // This file is generated. 2 | export const version = '1.13.1' 3 | -------------------------------------------------------------------------------- /packages/signals/signals/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the public API for this package. 3 | * We avoid using splat (*) exports so that we can control what is exposed. 4 | */ 5 | export { SignalsPlugin } from './plugin/signals-plugin' 6 | export { Signals } from './core/signals' 7 | export type { Signal } from '@segment/analytics-signals-runtime' 8 | export type { 9 | SignalsMiddleware, 10 | SignalsMiddlewareContext, 11 | } from './core/emitter' 12 | export type { 13 | ProcessSignal, 14 | AnalyticsRuntimePublicApi, 15 | SignalsPluginSettingsConfig, 16 | } from './types' 17 | -------------------------------------------------------------------------------- /packages/signals/signals/src/index.umd.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the entry point to a webpack bundle that can be loaded via a -------------------------------------------------------------------------------- /packages/signals/signals/src/test-helpers/mocks/analytics-mock.ts: -------------------------------------------------------------------------------- 1 | import { AnyAnalytics, EdgeFnCDNSettings } from '../../types' 2 | 3 | const edgeFnSettings: EdgeFnCDNSettings = { 4 | downloadURL: 'https://foo.com', 5 | version: 1, 6 | } 7 | 8 | export const analyticsMock: jest.Mocked = { 9 | settings: { 10 | writeKey: 'test', 11 | cdnSettings: { 12 | edgeFunction: edgeFnSettings, 13 | integrations: { 14 | 'Segment.io': { 15 | apiKey: '', 16 | }, 17 | }, 18 | }, 19 | }, 20 | alias: jest.fn(), 21 | identify: jest.fn(), 22 | screen: jest.fn(), 23 | group: jest.fn(), 24 | page: jest.fn(), 25 | track: jest.fn(), 26 | addSourceMiddleware: jest.fn(), 27 | reset: jest.fn(), 28 | on: jest.fn(), 29 | user: jest.fn().mockReturnValue({ 30 | id: jest.fn(), 31 | anonymousId: jest.fn(), 32 | }), 33 | } 34 | -------------------------------------------------------------------------------- /packages/signals/signals/src/test-helpers/mocks/factories.ts: -------------------------------------------------------------------------------- 1 | import { TargetedHTMLElement } from '@segment/analytics-signals-runtime' 2 | 3 | export const createMockTarget = ( 4 | partialTarget: Partial = {} 5 | ): TargetedHTMLElement => { 6 | return { 7 | attributes: {}, 8 | classList: [], 9 | id: 'test', 10 | labels: [], 11 | ...partialTarget, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/signals/signals/src/test-helpers/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './analytics-mock' 2 | -------------------------------------------------------------------------------- /packages/signals/signals/src/test-helpers/range.ts: -------------------------------------------------------------------------------- 1 | export const range = (n: number) => Array.from({ length: n }, (_, i) => i) 2 | -------------------------------------------------------------------------------- /packages/signals/signals/src/test-helpers/set-location.ts: -------------------------------------------------------------------------------- 1 | export const setLocation = (location: Partial = {}) => { 2 | Object.defineProperty(window, 'location', { 3 | value: { 4 | ...window.location, 5 | ...location, 6 | }, 7 | writable: true, 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /packages/signals/signals/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './analytics-api' 2 | export * from './process-signal' 3 | export * from './settings' 4 | -------------------------------------------------------------------------------- /packages/signals/signals/src/types/process-signal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Signal, 3 | WebRuntimeConstants, 4 | SignalsRuntime, 5 | } from '@segment/analytics-signals-runtime' 6 | 7 | /** 8 | * Types for the signals runtime 9 | */ 10 | export interface AnalyticsRuntimePublicApi { 11 | track: (...args: any[]) => void 12 | identify: (...args: any[]) => void 13 | alias: (...args: any[]) => void 14 | group: (...args: any[]) => void 15 | page: (...args: any[]) => void 16 | screen: (...args: any[]) => void 17 | reset: () => void 18 | } 19 | 20 | export type ProcessSignalScope = { 21 | analytics: AnalyticsRuntimePublicApi 22 | signals: SignalsRuntime 23 | } & typeof WebRuntimeConstants 24 | 25 | export interface ProcessSignal { 26 | (signal: Signal, ctx: ProcessSignalScope): void 27 | } 28 | -------------------------------------------------------------------------------- /packages/signals/signals/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ts-helpers' 2 | -------------------------------------------------------------------------------- /packages/signals/signals/src/utils/is-class.ts: -------------------------------------------------------------------------------- 1 | export const isClass = (value: any): value is NewableFunction => { 2 | return ( 3 | typeof value === 'function' && value.prototype.constructor !== undefined 4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /packages/signals/signals/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["**/__tests__/**", "**/test-helpers/**"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "outDir": "./dist/esm", 8 | "declarationDir": "./dist/types" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/signals/signals/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "exclude": ["node_modules", "dist"], 4 | "compilerOptions": { 5 | "module": "ESNext", // es6 modules 6 | "target": "ES2020", // don't down-compile *too much* -- if users are using webpack, they can always transpile this library themselves 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], // assume that consumers will be polyfilling at least down to es2020 8 | "moduleResolution": "node", 9 | "isolatedModules": true // ensure we are friendly to build systems 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/test-helpers/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../.eslintrc'], 4 | env: { 5 | node: true, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /packages/test-helpers/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@internal/config").lintStagedConfig 2 | -------------------------------------------------------------------------------- /packages/test-helpers/jest.config.js: -------------------------------------------------------------------------------- 1 | const { createJestTSConfig } = require('@internal/config') 2 | 3 | module.exports = createJestTSConfig(__dirname) 4 | -------------------------------------------------------------------------------- /packages/test-helpers/src/analytics/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cdn-settings-builder' 2 | -------------------------------------------------------------------------------- /packages/test-helpers/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Matches an ISO 8601 timestamp string. 3 | * @example 4 | * expect('2022-01-01T00:00:00.000Z').toEqual(expect.any(ISO_TIMESTAMP_REGEX)) 5 | */ 6 | export const ISO_TIMESTAMP_REGEX = 7 | /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ 8 | -------------------------------------------------------------------------------- /packages/test-helpers/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils' 2 | export * from './analytics' 3 | export * from './constants' 4 | export * as JestSerializers from './jest/serializers' 5 | -------------------------------------------------------------------------------- /packages/test-helpers/src/jest/serializers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './timestamp' 2 | -------------------------------------------------------------------------------- /packages/test-helpers/src/jest/serializers/timestamp.ts: -------------------------------------------------------------------------------- 1 | import { ISO_TIMESTAMP_REGEX } from '../../constants' 2 | 3 | /** 4 | * Jest snapshot serializer for ISO 8601 timestamp strings. 5 | */ 6 | export const jestSnapshotSerializerTimestamp: jest.SnapshotSerializerPlugin = { 7 | test(value: any) { 8 | return typeof value === 'string' && ISO_TIMESTAMP_REGEX.test(value) 9 | }, 10 | print() { 11 | return '' 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /packages/test-helpers/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sleep' 2 | export * from './promise-timeout' 3 | -------------------------------------------------------------------------------- /packages/test-helpers/src/utils/promise-timeout.ts: -------------------------------------------------------------------------------- 1 | export function promiseTimeout( 2 | promise: Promise, 3 | timeout: number, 4 | errorMsg?: string 5 | ): Promise { 6 | return new Promise((resolve, reject) => { 7 | const timeoutId = setTimeout(() => { 8 | reject(Error(errorMsg ?? 'Promise timed out')) 9 | }, timeout) 10 | 11 | promise 12 | .then((val) => { 13 | clearTimeout(timeoutId) 14 | return resolve(val) 15 | }) 16 | .catch(reject) 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /packages/test-helpers/src/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (time: number): Promise => 2 | new Promise((resolve) => { 3 | setTimeout(resolve, time) 4 | }) 5 | -------------------------------------------------------------------------------- /packages/test-helpers/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"], 4 | "exclude": ["**/__tests__/**", "**/*.test.*"], 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "outDir": "./dist/esm", 8 | "declarationDir": "./dist/types" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/test-helpers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "exclude": ["node_modules", "dist"], 4 | "compilerOptions": { 5 | "resolveJsonModule": true, 6 | "module": "esnext", 7 | "target": "ES5", 8 | "moduleResolution": "node", 9 | "lib": ["es2020"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /playgrounds/README.md: -------------------------------------------------------------------------------- 1 | # Application Playground 2 | 3 | These applications are not meant to be vanilla examples. Please refer to the individual READMEs for more information. 4 | -------------------------------------------------------------------------------- /playgrounds/next-playground/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type { import('eslint').Linter.Config } */ 2 | module.exports = { 3 | extends: ['../../.eslintrc', 'plugin:@next/next/recommended'], 4 | env: { 5 | browser: true, 6 | node: true, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /playgrounds/next-playground/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | *.tsbuildinfo 34 | 35 | # vercel 36 | .vercel 37 | 38 | # @builder.io/partytown 39 | public/~partytown 40 | -------------------------------------------------------------------------------- /playgrounds/next-playground/.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | // https://nextjs.org/docs/basic-features/eslint#lint-staged 2 | const path = require('path') 3 | 4 | const buildEslintCommand = (filenames) => 5 | `next lint --fix --file ${filenames 6 | .map((f) => path.relative(process.cwd(), f)) 7 | .join(' --file ')}` 8 | 9 | module.exports = { 10 | '*.{js,jsx,ts,tsx}': [buildEslintCommand], 11 | } 12 | -------------------------------------------------------------------------------- /playgrounds/next-playground/README.md: -------------------------------------------------------------------------------- 1 | ### ⚠️ This is not a vanilla analytics.js-next.js example. 2 | If you're looking for how to implement Analytics.js with Next.js, see: 3 | - https://github.com/vercel/next.js/tree/canary/examples/with-segment-analytics 4 | - https://github.com/vercel/next.js/tree/canary/examples/with-segment-analytics-pages-router 5 | 6 | ### Getting Started 7 | First, run the development server: 8 | 9 | ```bash 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | -------------------------------------------------------------------------------- /playgrounds/next-playground/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /playgrounds/next-playground/next.config.js: -------------------------------------------------------------------------------- 1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({ 2 | enabled: process.env.ANALYZE === 'true', 3 | }) 4 | 5 | module.exports = withBundleAnalyzer({ 6 | webpack: (config) => { 7 | if (config.mode === 'development') { 8 | config.module.rules.push({ 9 | test: /\.js$/, 10 | use: ['source-map-loader'], 11 | enforce: 'pre', 12 | }) 13 | if (!Array.isArray(config.ignoreWarnings)) { 14 | config.ignoreWarnings = [] 15 | } 16 | 17 | config.ignoreWarnings.push(/Failed to parse source map/) 18 | } 19 | 20 | return config 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /playgrounds/next-playground/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import '../styles/dracula/dracula-ui.css' 3 | import '../styles/dracula/prism.css' 4 | import '../styles/logs-table.css' 5 | 6 | export default function ExampleApp({ Component, pageProps }) { 7 | return 8 | } 9 | -------------------------------------------------------------------------------- /playgrounds/next-playground/pages/iframe/childPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AnalyticsProvider } from '../../context/analytics' 3 | 4 | const ChildPage: React.FC = () => { 5 | return
Hello world!
6 | } 7 | 8 | export default () => ( 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /playgrounds/next-playground/pages/iframe/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AnalyticsProvider } from '../../context/analytics' 3 | 4 | function Iframe(): React.ReactElement { 5 | return ( 6 | 13 | ) 14 | } 15 | 16 | export default () => ( 17 | 18 |