diff --git a/.changeset/cool-peaches-pay.md b/.changeset/cool-peaches-pay.md new file mode 100644 index 000000000..3ad96cd01 --- /dev/null +++ b/.changeset/cool-peaches-pay.md @@ -0,0 +1,5 @@ +--- +'@segment/analytics-next': minor +--- + +Set timezone and allow userAgentData to be overridden diff --git a/.changeset/forty-toes-stare.md b/.changeset/forty-toes-stare.md new file mode 100644 index 000000000..24fc716c7 --- /dev/null +++ b/.changeset/forty-toes-stare.md @@ -0,0 +1,6 @@ +--- +'@segment/analytics-consent-tools': patch +'@segment/analytics-consent-wrapper-onetrust': patch +--- + +Support older browsers diff --git a/.changeset/mean-apricots-hammer.md b/.changeset/mean-apricots-hammer.md deleted file mode 100644 index 76b8b9065..000000000 --- a/.changeset/mean-apricots-hammer.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@segment/analytics-next': minor -'@segment/analytics-core': minor ---- - -Adds `globalAnalyticsKey` option for setting custom global window buffers diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74a89229b..6bd3821a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: key: ${{ runner.os }}-turbo-${{ github.sha }} restore-keys: | ${{ runner.os }}-turbo- - - run: yarn turbo run --filter='consent-tools-integration-tests' test:intg + - run: yarn turbo run --filter='consent-tools-integration-tests' test:int diff --git a/package.json b/package.json index 64dee97c1..9d92d092f 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "prettier": "^2.6.2", "ts-jest": "^28.0.4", "ts-node": "^10.8.0", - "turbo": "^1.3.1", + "turbo": "^1.10.14", "typescript": "^4.7.0", "webpack": "^5.76.0", "webpack-dev-server": "^4.15.1" diff --git a/packages/browser/CHANGELOG.md b/packages/browser/CHANGELOG.md index e0e2b4ffe..954808360 100644 --- a/packages/browser/CHANGELOG.md +++ b/packages/browser/CHANGELOG.md @@ -1,5 +1,15 @@ # @segment/analytics-next +## 1.56.0 + +### Minor Changes + +- [#928](https://github.com/segmentio/analytics-next/pull/928) [`7f4232c`](https://github.com/segmentio/analytics-next/commit/7f4232cbdb60a4475c565e5d262b25182e47baf4) Thanks [@oscb](https://github.com/oscb)! - Adds `globalAnalyticsKey` option for setting custom global window buffers + +### Patch Changes + +- [#949](https://github.com/segmentio/analytics-next/pull/949) [`fcf42f6`](https://github.com/segmentio/analytics-next/commit/fcf42f68b4226b55417bbaeb6305d33129ede96d) Thanks [@silesky](https://github.com/silesky)! - Fix regression where we no longer export UniversalStorage (used in destinations) + ## 1.55.0 ### Minor Changes diff --git a/packages/browser/package.json b/packages/browser/package.json index 015645423..e2df1c8fc 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-next", - "version": "1.55.0", + "version": "1.56.0", "repository": { "type": "git", "url": "https://github.com/segmentio/analytics-next", diff --git a/packages/browser/src/generated/version.ts b/packages/browser/src/generated/version.ts index 5acc3a383..3e0d2d55c 100644 --- a/packages/browser/src/generated/version.ts +++ b/packages/browser/src/generated/version.ts @@ -1,2 +1,2 @@ // This file is generated. -export const version = '1.55.0' +export const version = '1.56.0' diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 625d8e91a..fdd552aa8 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -10,3 +10,4 @@ export * from './core/user' export type { AnalyticsSnippet } from './browser/standalone-interface' export type { MiddlewareFunction } from './plugins/middleware' export { getGlobalAnalytics } from './lib/global-analytics-helper' +export { UniversalStorage, Store, StorageObject } from './core/storage' diff --git a/packages/browser/src/plugins/page-enrichment/__tests__/index.test.ts b/packages/browser/src/plugins/page-enrichment/__tests__/index.test.ts index 90723cfcd..e0bee7514 100644 --- a/packages/browser/src/plugins/page-enrichment/__tests__/index.test.ts +++ b/packages/browser/src/plugins/page-enrichment/__tests__/index.test.ts @@ -6,6 +6,11 @@ import { pick } from '../../../lib/pick' import { SegmentioSettings } from '../../segmentio' import { version } from '../../../generated/version' import { CoreExtraContext } from '@segment/analytics-core' +import { UADataValues } from '../../../lib/client-hints/interfaces' +import { + highEntropyTestData, + lowEntropyTestData, +} from '../../../test-helpers/fixtures/client-hints' let ajs: Analytics @@ -262,6 +267,29 @@ describe('pageDefaults', () => { describe('Other visitor metadata', () => { let options: SegmentioSettings let analytics: Analytics + ;(window.navigator as any).userAgentData = { + ...lowEntropyTestData, + getHighEntropyValues: jest + .fn() + .mockImplementation((hints: string[]): Promise => { + let result = {} + Object.entries(highEntropyTestData).forEach(([k, v]) => { + if (hints.includes(k)) { + result = { + ...result, + [k]: v, + } + } + }) + return Promise.resolve({ + ...lowEntropyTestData, + ...result, + }) + }), + toJSON: jest.fn(() => { + return lowEntropyTestData + }), + } const amendSearchParams = (search?: any): CoreExtraContext => ({ page: { search }, @@ -283,6 +311,11 @@ describe('Other visitor metadata', () => { } }) + it('should add .timezone', async () => { + const ctx = await analytics.track('test') + assert(typeof ctx.event.context?.timezone === 'string') + }) + it('should add .library', async () => { const ctx = await analytics.track('test') assert(ctx.event.context?.library) @@ -313,6 +346,11 @@ describe('Other visitor metadata', () => { assert(userAgent1 === userAgent2) }) + it('should add .userAgentData when available', async () => { + const ctx = await analytics.track('event') + expect(ctx.event.context?.userAgentData).toEqual(lowEntropyTestData) + }) + it('should add .locale', async () => { const ctx = await analytics.track('test') assert(ctx.event.context?.locale === navigator.language) diff --git a/packages/browser/src/plugins/page-enrichment/index.ts b/packages/browser/src/plugins/page-enrichment/index.ts index 91cf1162f..b113dc337 100644 --- a/packages/browser/src/plugins/page-enrichment/index.ts +++ b/packages/browser/src/plugins/page-enrichment/index.ts @@ -10,6 +10,8 @@ import { tld } from '../../core/user/tld' import { gracefulDecodeURIComponent } from '../../core/query-string/gracefulDecodeURIComponent' import { CookieStorage, UniversalStorage } from '../../core/storage' import { Analytics } from '../../core/analytics' +import { clientHints } from '../../lib/client-hints' +import { UADataValues } from '../../lib/client-hints/interfaces' interface PageDefault { [key: string]: unknown @@ -174,13 +176,21 @@ function referrerId( class PageEnrichmentPlugin implements Plugin { private instance!: Analytics + private userAgentData: UADataValues | undefined name = 'Page Enrichment' type: PluginType = 'before' version = '0.1.0' isLoaded = () => true - load = (_ctx: Context, instance: Analytics) => { + load = async (_ctx: Context, instance: Analytics) => { this.instance = instance + try { + this.userAgentData = await clientHints( + this.instance.options.highEntropyValuesClientHints + ) + } catch (_) { + // if client hints API doesn't return anything leave undefined + } return Promise.resolve() } @@ -213,6 +223,7 @@ class PageEnrichmentPlugin implements Plugin { const query: string = evtCtx.page.search || '' evtCtx.userAgent = navigator.userAgent + evtCtx.userAgentData = this.userAgentData // @ts-ignore const locale = navigator.userLanguage || navigator.language @@ -241,6 +252,12 @@ class PageEnrichmentPlugin implements Plugin { this.instance.options.disableClientPersistence ?? false ) + try { + evtCtx.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone + } catch (_) { + // If browser doesn't have support leave timezone undefined + } + return ctx } diff --git a/packages/browser/src/plugins/segmentio/__tests__/index.test.ts b/packages/browser/src/plugins/segmentio/__tests__/index.test.ts index d9858cb57..fdb9be1d8 100644 --- a/packages/browser/src/plugins/segmentio/__tests__/index.test.ts +++ b/packages/browser/src/plugins/segmentio/__tests__/index.test.ts @@ -5,11 +5,6 @@ import { Analytics } from '../../../core/analytics' import { Plugin } from '../../../core/plugin' import { pageEnrichment } from '../../page-enrichment' import cookie from 'js-cookie' -import { UADataValues } from '../../../lib/client-hints/interfaces' -import { - highEntropyTestData, - lowEntropyTestData, -} from '../../../test-helpers/fixtures/client-hints' jest.mock('unfetch', () => { return jest.fn() @@ -24,29 +19,6 @@ describe('Segment.io', () => { beforeEach(async () => { jest.resetAllMocks() jest.restoreAllMocks() - ;(window.navigator as any).userAgentData = { - ...lowEntropyTestData, - getHighEntropyValues: jest - .fn() - .mockImplementation((hints: string[]): Promise => { - let result = {} - Object.entries(highEntropyTestData).forEach(([k, v]) => { - if (hints.includes(k)) { - result = { - ...result, - [k]: v, - } - } - }) - return Promise.resolve({ - ...lowEntropyTestData, - ...result, - }) - }), - toJSON: jest.fn(() => { - return lowEntropyTestData - }), - } options = { apiKey: 'foo' } analytics = new Analytics({ writeKey: options.apiKey }) @@ -199,14 +171,6 @@ describe('Segment.io', () => { assert(body.traits == null) assert(body.timestamp) }) - - it('should add userAgentData when available', async () => { - await analytics.track('event') - const [_, params] = spyMock.mock.calls[0] - const body = JSON.parse(params.body) - - expect(body.context?.userAgentData).toEqual(lowEntropyTestData) - }) }) describe('#group', () => { diff --git a/packages/browser/src/plugins/segmentio/index.ts b/packages/browser/src/plugins/segmentio/index.ts index 2dc8b2967..38f012d74 100644 --- a/packages/browser/src/plugins/segmentio/index.ts +++ b/packages/browser/src/plugins/segmentio/index.ts @@ -12,8 +12,6 @@ import standard, { StandardDispatcherConfig } from './fetch-dispatcher' import { normalize } from './normalize' import { scheduleFlush } from './schedule-flush' import { SEGMENT_API_HOST } from '../../core/constants' -import { clientHints } from '../../lib/client-hints' -import { UADataValues } from '../../lib/client-hints/interfaces' type DeliveryStrategy = | { @@ -52,11 +50,11 @@ function onAlias(analytics: Analytics, json: JSON): JSON { return json } -export async function segmentio( +export function segmentio( analytics: Analytics, settings?: SegmentioSettings, integrations?: LegacySettings['integrations'] -): Promise { +): Plugin { // Attach `pagehide` before buffer is created so that inflight events are added // to the buffer before the buffer persists events in its own `pagehide` handler. window.addEventListener('pagehide', () => { @@ -86,15 +84,6 @@ export async function segmentio( ? batch(apiHost, deliveryStrategy.config) : standard(deliveryStrategy?.config as StandardDispatcherConfig) - let userAgentData: UADataValues | undefined - try { - userAgentData = await clientHints( - analytics.options.highEntropyValuesClientHints - ) - } catch { - userAgentData = undefined - } - async function send(ctx: Context): Promise { if (isOffline()) { buffer.push(ctx) @@ -107,10 +96,6 @@ export async function segmentio( const path = ctx.event.type.charAt(0) - if (userAgentData && ctx.event.context) { - ctx.event.context.userAgentData = userAgentData - } - let json = toFacade(ctx.event).json() if (ctx.event.type === 'track') { diff --git a/packages/config-webpack/package.json b/packages/config-webpack/package.json index c243c7314..fabafc794 100644 --- a/packages/config-webpack/package.json +++ b/packages/config-webpack/package.json @@ -11,6 +11,7 @@ "@types/circular-dependency-plugin": "^5", "babel-loader": "^8.0.0", "circular-dependency-plugin": "^5.2.2", + "ecma-version-validator-webpack-plugin": "^1.2.1", "terser-webpack-plugin": "^5.1.4", "webpack": "^5.76.0", "webpack-cli": "^4.8.0", diff --git a/packages/config-webpack/webpack.config.common.js b/packages/config-webpack/webpack.config.common.js index 4710464c2..d69005c4d 100644 --- a/packages/config-webpack/webpack.config.common.js +++ b/packages/config-webpack/webpack.config.common.js @@ -1,5 +1,8 @@ const TerserPlugin = require('terser-webpack-plugin') const CircularDependencyPlugin = require('circular-dependency-plugin') +const { + ECMAVersionValidatorPlugin, +} = require('ecma-version-validator-webpack-plugin') const isProd = process.env.NODE_ENV === 'production' const isWatch = process.env.WATCH === 'true' @@ -15,6 +18,7 @@ module.exports = { devtool: 'source-map', stats: isWatch ? 'errors-warnings' : 'normal', mode: isProd ? 'production' : 'development', + target: ['web', 'es5'], // target es5 for ie11 support (generates module boilerplate in es5) module: { rules: [ { @@ -37,7 +41,6 @@ module.exports = { }, }, ], - exclude: /node_modules/, }, ], }, @@ -67,5 +70,6 @@ module.exports = { new CircularDependencyPlugin({ failOnError: true, }), + new ECMAVersionValidatorPlugin({ ecmaVersion: 5 }), // ensure our js bundle only contains syntax supported in ie11. This does not check polyfills. ], } diff --git a/packages/consent/consent-tools-integration-tests/README.md b/packages/consent/consent-tools-integration-tests/README.md index e10909e33..65e606551 100644 --- a/packages/consent/consent-tools-integration-tests/README.md +++ b/packages/consent/consent-tools-integration-tests/README.md @@ -16,5 +16,5 @@ Why is this using wd.io instead of playwright? ## Development ### Build, start server, run tests (and exit gracefully) ``` -yarn . test:intg +yarn . test:int ``` diff --git a/packages/consent/consent-tools-integration-tests/package.json b/packages/consent/consent-tools-integration-tests/package.json index 7511b9245..7b8933df1 100644 --- a/packages/consent/consent-tools-integration-tests/package.json +++ b/packages/consent/consent-tools-integration-tests/package.json @@ -3,7 +3,7 @@ "private": true, "scripts": { ".": "yarn run -T turbo run --filter=@internal/consent-tools-integration-tests...", - "test:intg": "yarn wdio:local", + "test:int": "yarn wdio:local", "build": "webpack", "watch": "yarn build --watch", "wdio:local": "wdio wdio.conf.local.ts", diff --git a/packages/consent/consent-tools-integration-tests/src/page-bundles/consent-tools-vanilla/index.ts b/packages/consent/consent-tools-integration-tests/src/page-bundles/consent-tools-vanilla/index.ts index 5e56c29b3..f04602e81 100644 --- a/packages/consent/consent-tools-integration-tests/src/page-bundles/consent-tools-vanilla/index.ts +++ b/packages/consent/consent-tools-integration-tests/src/page-bundles/consent-tools-vanilla/index.ts @@ -1,14 +1,10 @@ import { AnalyticsBrowser } from '@segment/analytics-next' import { createWrapper } from '@segment/analytics-consent-tools' -const fakeCategories = { Advertising: true, Analytics: true } +const fakeCategories = { FooCategory1: true, FooCategory2: true } const withCMP = createWrapper({ getCategories: () => fakeCategories, - integrationCategoryMappings: { - Fullstory: ['Analytics'], - 'Actions Amplitude': ['Advertising'], - }, }) const analytics = new AnalyticsBrowser() diff --git a/packages/consent/consent-tools-integration-tests/src/page-objects/base-page.ts b/packages/consent/consent-tools-integration-tests/src/page-objects/base-page.ts index 569028cce..0da5da67d 100644 --- a/packages/consent/consent-tools-integration-tests/src/page-objects/base-page.ts +++ b/packages/consent/consent-tools-integration-tests/src/page-objects/base-page.ts @@ -36,13 +36,13 @@ export abstract class BasePage { { creationName: 'FullStory', consentSettings: { - categories: ['Analytics'], + categories: ['FooCategory2'], }, }, { creationName: 'Actions Amplitude', consentSettings: { - categories: ['Advertising'], + categories: ['FooCategory1'], }, } ) diff --git a/packages/consent/consent-tools-integration-tests/src/tests/consent-tools-vanilla.test.ts b/packages/consent/consent-tools-integration-tests/src/tests/consent-tools-vanilla.test.ts index 62c9c1154..2043ecdf0 100644 --- a/packages/consent/consent-tools-integration-tests/src/tests/consent-tools-vanilla.test.ts +++ b/packages/consent/consent-tools-integration-tests/src/tests/consent-tools-vanilla.test.ts @@ -1,7 +1,10 @@ +/** + * Tests targeting @segment/analytics-consent-tools + */ + import page from '../page-objects/consent-tools-vanilla' import { expect } from 'expect' -// Verify that the consent tools wrapper is working as expected (no OneTrust) it('should stamp each event', async () => { await page.load() @@ -11,8 +14,8 @@ it('should stamp each event', async () => { expect((ctx.event.context as any).consent).toEqual({ categoryPreferences: { - Advertising: true, - Analytics: true, + FooCategory1: true, + FooCategory2: true, }, }) }) diff --git a/packages/consent/consent-tools-integration-tests/src/tests/onetrust.test.ts b/packages/consent/consent-tools-integration-tests/src/tests/onetrust.test.ts index 40b73aa76..bc8d2227c 100644 --- a/packages/consent/consent-tools-integration-tests/src/tests/onetrust.test.ts +++ b/packages/consent/consent-tools-integration-tests/src/tests/onetrust.test.ts @@ -1,6 +1,7 @@ -// Verify that @segment/analytics-consent-wrapper-onetrust is working as expected +/** + * Tests targeting @segment/analytics-consent-wrapper-onetrust + */ -/* eslint-disable @typescript-eslint/no-floating-promises */ import page from '../page-objects/onetrust' import { expect } from 'expect' diff --git a/packages/consent/consent-tools/README.md b/packages/consent/consent-tools/README.md index a9fa8ac3d..658c6803d 100644 --- a/packages/consent/consent-tools/README.md +++ b/packages/consent/consent-tools/README.md @@ -7,23 +7,41 @@ import { createWrapper, resolveWhen } from '@segment/analytics-consent-tools' export const withCMP = createWrapper({ + + // Wrapper waits to load segment / get categories until this function returns / resolves shouldLoad: (ctx) => { + const CMP = await getCMP() await resolveWhen( - () => window.CMP !== undefined && !window.CMP.popUpVisible(), + () => !CMP.popUpVisible(), 500 ) + // Optional -- for granular control of initialization if (noConsentNeeded) { ctx.abort({ loadSegmentNormally: true }) } else if (allTrackingDisabled) { ctx.abort({ loadSegmentNormally: false }) } }, + getCategories: () => { - // e.g. { Advertising: true, Functional: false } - return normalizeCategories(window.CMP.consentedCategories()) + const CMP = await getCMP() + return normalizeCategories(CMP.consentedCategories()) // Expected format: { foo: true, bar: false } + }, + + registerOnConsentChanged: (setCategories) => { + const CMP = await getCMP() + CMP.onConsentChanged((event) => { + setCategories(normalizeCategories(event.detail)) + }) }, }) + + +const getCMP = async () => { + await resolveWhen(() => window.CMP !== undefined, 500) + return window.CMP +} ``` ## Wrapper Usage API @@ -36,9 +54,7 @@ import { AnalyticsBrowser } from '@segment/analytics-next' export const analytics = new AnalyticsBrowser() -withCMP(analytics) - -analytics.load({ +withCMP(analytics).load({ writeKey: ' }) @@ -58,9 +74,7 @@ analytics.load({ ```js import { withCMP } from './wrapper' -withCMP(window.analytics) - -window.analytics.load(' }) > - +