From 86bd79d4333a5dc4a23e84176062e802be259407 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Thu, 24 Aug 2023 11:50:29 -0700 Subject: [PATCH 01/50] feat: init with diagnostciProvider --- packages/analytics-browser/src/config.ts | 5 +- .../src/plugins/destination.ts | 21 +++++++ packages/analytics-core/src/index.ts | 2 + .../analytics-core/src/plugins/diagnostic.ts | 55 +++++++++++++++++++ .../analytics-types/src/config/browser.ts | 2 + .../analytics-types/src/diagnostic-event.ts | 10 ++++ packages/analytics-types/src/index.ts | 3 +- packages/analytics-types/src/plugin.ts | 10 +++- yarn.lock | 17 ++++++ 9 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 packages/analytics-browser/src/plugins/destination.ts create mode 100644 packages/analytics-core/src/plugins/diagnostic.ts create mode 100644 packages/analytics-types/src/diagnostic-event.ts diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index 74bd6e4de..9158c9519 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -13,8 +13,9 @@ import { IngestionMetadata, IdentityStorageType, ServerZoneType, + DiagnosticPlugin, } from '@amplitude/analytics-types'; -import { Config, Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; +import { Config, Diagnostic, Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; import { CookieStorage, getCookieName, FetchTransport, getQueryParams } from '@amplitude/analytics-client-common'; import { LocalStorage } from './storage/local-storage'; @@ -76,6 +77,7 @@ export class BrowserConfig extends Config implements IBrowserConfig { public transport: 'fetch' | 'xhr' | 'beacon' = 'fetch', public useBatch: boolean = false, userId?: string, + public diagnosticProvider: DiagnosticPlugin = new Diagnostic(), ) { super({ apiKey, storageProvider, transportProvider: createTransport(transport) }); this._cookieStorage = cookieStorage; @@ -248,6 +250,7 @@ export const useBrowserConfig = async ( options.transport, options.useBatch, userId, + options.diagnosticProvider, ); }; diff --git a/packages/analytics-browser/src/plugins/destination.ts b/packages/analytics-browser/src/plugins/destination.ts new file mode 100644 index 000000000..e0e811083 --- /dev/null +++ b/packages/analytics-browser/src/plugins/destination.ts @@ -0,0 +1,21 @@ +import { Destination as CoreDestination, buildResult } from '@amplitude/analytics-core'; +import { BrowserConfig } from '../../src/config'; +import { DestinationContext as Context } from '@amplitude/analytics-types'; + +export class Destination extends CoreDestination { + // this.config is defined in setup() which will always be called first + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + config: BrowserConfig; + + async setup(config: BrowserConfig): Promise { + this.config = config; + return super.setup(config); + } + + async fulfillRequest(list: Context[], code: number, message: string) { + await this.config.diagnosticProvider.track(list.length, code, message); + this.saveEvents(); + list.forEach((context) => context.callback(buildResult(context.event, code, message))); + } +} diff --git a/packages/analytics-core/src/index.ts b/packages/analytics-core/src/index.ts index 3106a5d33..7f4e8a156 100644 --- a/packages/analytics-core/src/index.ts +++ b/packages/analytics-core/src/index.ts @@ -2,6 +2,7 @@ export { AmplitudeCore } from './core-client'; export { Identify } from './identify'; export { Revenue } from './revenue'; export { Destination } from './plugins/destination'; +export { Diagnostic } from './plugins/diagnostic'; export { Config } from './config'; export { Logger } from './logger'; export { AMPLITUDE_PREFIX, STORAGE_PREFIX } from './constants'; @@ -11,3 +12,4 @@ export { UUID } from './utils/uuid'; export { MemoryStorage } from './storage/memory'; export { BaseTransport } from './transports/base'; export { createIdentifyEvent } from './utils/event-builder'; +export { buildResult } from './utils/result-builder'; diff --git a/packages/analytics-core/src/plugins/diagnostic.ts b/packages/analytics-core/src/plugins/diagnostic.ts new file mode 100644 index 000000000..897b9f56e --- /dev/null +++ b/packages/analytics-core/src/plugins/diagnostic.ts @@ -0,0 +1,55 @@ +import { Config, CoreClient, DiagnosticPlugin, Event, Result } from '@amplitude/analytics-types'; +import fetch from 'node-fetch'; + +export class Diagnostic implements DiagnosticPlugin { + name = '@amplitude/plugin-diagnostic'; + type = 'destination' as const; + + constructor() { + // do something + } + + execute(_context: Event): Promise { + throw new Error('Method not implemented.'); + // it is called by timeline when client.track() + // do nothing when execute is called + } + flush?(): Promise { + throw new Error('Method not implemented.'); + // should flush all unsent events + } + setup?(_config: Config, _client: CoreClient): Promise { + throw new Error('Method not implemented.'); + } + teardown?(): Promise { + throw new Error('Method not implemented.'); + } + async track(eventCount: number, code: number, message: string) { + // add event to queue + // https://github.com/amplitude/Amplitude-TypeScript/blob/502a080b6eca2bc390b5d8076f24b9137d213f89/packages/analytics-core/src/plugins/destination.ts#L70-L80 + + const payload = { + time: Date.now(), + event_properties: { + response_error_code: code, + trigger: message, + action: 'drop events', + event_count: eventCount, + }, + library: 'diagnostic-test-library', + }; + + const serverUrl = 'http://localhost:8000'; + + const body = { + headers: { + 'Content-Type': 'application/json', + Accept: '*/*', + }, + body: JSON.stringify(payload), + method: 'POST', + }; + + await fetch(serverUrl, body); + } +} diff --git a/packages/analytics-types/src/config/browser.ts b/packages/analytics-types/src/config/browser.ts index 0ce182be1..0374ab54e 100644 --- a/packages/analytics-types/src/config/browser.ts +++ b/packages/analytics-types/src/config/browser.ts @@ -3,6 +3,7 @@ import { IdentityStorageType, Storage } from '../storage'; import { Transport } from '../transport'; import { Config } from './core'; import { PageTrackingOptions } from '../page-view-tracking'; +import { DiagnosticPlugin } from '../plugin'; export interface BrowserConfig extends ExternalBrowserConfig, InternalBrowserConfig {} @@ -18,6 +19,7 @@ export interface ExternalBrowserConfig extends Config { trackingOptions: TrackingOptions; transport?: 'fetch' | 'xhr' | 'beacon'; userId?: string; + diagnosticProvider?: DiagnosticPlugin; } interface InternalBrowserConfig { diff --git a/packages/analytics-types/src/diagnostic-event.ts b/packages/analytics-types/src/diagnostic-event.ts new file mode 100644 index 000000000..89f1da9a7 --- /dev/null +++ b/packages/analytics-types/src/diagnostic-event.ts @@ -0,0 +1,10 @@ +export interface DiagnosticEvent { + time: number; + event_properties: { + response_error_code: number; + trigger: string; + action: string; + event_count: number; + }; + library: string; +} diff --git a/packages/analytics-types/src/index.ts b/packages/analytics-types/src/index.ts index 8d5364b7e..66e07a4dd 100644 --- a/packages/analytics-types/src/index.ts +++ b/packages/analytics-types/src/index.ts @@ -1,5 +1,6 @@ export { AmplitudeReturn } from './amplitude-promise'; export { BaseEvent, EventOptions } from './base-event'; +export { DiagnosticEvent } from './diagnostic-event'; export { Campaign, CampaignParser, @@ -48,7 +49,7 @@ export { Logger, LogLevel, LogConfig, DebugContext } from './logger'; export { Payload } from './payload'; export { Plan } from './plan'; export { IngestionMetadata } from './ingestion-metadata'; -export { Plugin, BeforePlugin, EnrichmentPlugin, DestinationPlugin, PluginType } from './plugin'; +export { Plugin, BeforePlugin, EnrichmentPlugin, DestinationPlugin, DiagnosticPlugin, PluginType } from './plugin'; export { Result } from './result'; export { Response, SuccessResponse, InvalidResponse, PayloadTooLargeResponse, RateLimitResponse } from './response'; export { QueueProxy, InstanceProxy } from './proxy'; diff --git a/packages/analytics-types/src/plugin.ts b/packages/analytics-types/src/plugin.ts index ac7b6e1fd..aa73712bc 100644 --- a/packages/analytics-types/src/plugin.ts +++ b/packages/analytics-types/src/plugin.ts @@ -31,4 +31,12 @@ export interface DestinationPlugin extends PluginBas flush?(): Promise; } -export type Plugin = BeforePlugin | EnrichmentPlugin | DestinationPlugin; +export interface DiagnosticPlugin extends DestinationPlugin { + track(eventCount: number, code: number, message: string): Promise; +} + +export type Plugin = + | BeforePlugin + | EnrichmentPlugin + | DestinationPlugin + | DiagnosticPlugin; diff --git a/yarn.lock b/yarn.lock index 2be390f76..320860c39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3950,6 +3950,14 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== +"@types/node-fetch@^2.6.4": + version "2.6.4" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660" + integrity sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + "@types/node@*", "@types/node@^18.11.14": version "18.11.14" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.14.tgz#a8571b25f3a31e9ded14e3ab9488509adef831d8" @@ -6828,6 +6836,15 @@ for-in@^1.0.2: resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" From b66a468707e5cda359c8cf82f1f64fad507c31ba Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Fri, 25 Aug 2023 10:52:38 -0700 Subject: [PATCH 02/50] test: add diagnostic --- .../analytics-browser/test/config.test.ts | 4 ++ .../test/plugins/destination.test.ts | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 packages/analytics-browser/test/plugins/destination.test.ts diff --git a/packages/analytics-browser/test/config.test.ts b/packages/analytics-browser/test/config.test.ts index a20df4227..c32caf2b5 100644 --- a/packages/analytics-browser/test/config.test.ts +++ b/packages/analytics-browser/test/config.test.ts @@ -20,6 +20,7 @@ describe('config', () => { const someLocalStorage: LocalStorageModule.LocalStorage = expect.any( LocalStorageModule.LocalStorage, ) as LocalStorageModule.LocalStorage; + const someDiagnosticProvider: core.Diagnostic = expect.any(core.Diagnostic) as core.Diagnostic; let apiKey = ''; @@ -73,6 +74,7 @@ describe('config', () => { transport: 'fetch', transportProvider: new FetchTransport(), useBatch: false, + diagnosticProvider: someDiagnosticProvider, }); }); @@ -128,6 +130,7 @@ describe('config', () => { transport: 'fetch', transportProvider: new FetchTransport(), useBatch: false, + diagnosticProvider: someDiagnosticProvider, }); expect(getTopLevelDomain).toHaveBeenCalledTimes(1); }); @@ -213,6 +216,7 @@ describe('config', () => { transport: 'fetch', transportProvider: new FetchTransport(), useBatch: false, + diagnosticProvider: someDiagnosticProvider, }); }); diff --git a/packages/analytics-browser/test/plugins/destination.test.ts b/packages/analytics-browser/test/plugins/destination.test.ts new file mode 100644 index 000000000..9fc5f7ea6 --- /dev/null +++ b/packages/analytics-browser/test/plugins/destination.test.ts @@ -0,0 +1,46 @@ +import { Destination } from '../../src/plugins/destination'; +import { BrowserConfig, useBrowserConfig } from '../../src/config'; +import * as core from '@amplitude/analytics-core'; +import { AmplitudeBrowser } from '../../src/browser-client'; + +describe('destination', () => { + const apiKey = core.UUID(); + const someDiagnosticProvider: core.Diagnostic = expect.any(core.Diagnostic) as core.Diagnostic; + + describe('setup', () => { + test('should setup plugin', async () => { + const destination = new Destination(); + const config = (await useBrowserConfig(apiKey, undefined, new AmplitudeBrowser())) as BrowserConfig; + await destination.setup(config); + expect(destination.config.diagnosticProvider).toEqual(someDiagnosticProvider); + }); + }); + + describe('fulfillRequest', () => { + test('should track diagnostics', async () => { + const destination = new Destination(); + const mockDiagnosticProvider = { + type: 'destination' as const, + execute: jest.fn(), + flush: jest.fn(), + track: jest.fn(), + }; + const config = (await useBrowserConfig(apiKey, undefined, new AmplitudeBrowser())) as BrowserConfig; + config.diagnosticProvider = mockDiagnosticProvider; + destination.config = config; + const callback = jest.fn(); + const context = { + attempts: 0, + callback, + event: { + event_type: 'event_type', + }, + timeout: 0, + }; + await destination.fulfillRequest([context], 200, 'success'); + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(core.buildResult(context.event, 200, 'success')); + expect(mockDiagnosticProvider.track).toHaveBeenCalledTimes(1); + }); + }); +}); From cab2ecd19c63dcbee1083b9b883f2bce2ed23588 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Fri, 25 Aug 2023 17:02:27 -0700 Subject: [PATCH 03/50] fix: init Destination with config --- .../analytics-browser/src/browser-client.ts | 5 +++-- packages/analytics-browser/src/config.ts | 2 +- .../src/plugins/destination.ts | 9 ++++---- .../test/plugins/destination.test.ts | 21 ++++++++++--------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/analytics-browser/src/browser-client.ts b/packages/analytics-browser/src/browser-client.ts index 8c7c2b3c0..3bf0aed68 100644 --- a/packages/analytics-browser/src/browser-client.ts +++ b/packages/analytics-browser/src/browser-client.ts @@ -1,4 +1,4 @@ -import { AmplitudeCore, Destination, Identify, returnWrapper, Revenue, UUID } from '@amplitude/analytics-core'; +import { AmplitudeCore, Identify, returnWrapper, Revenue, UUID } from '@amplitude/analytics-core'; import { getAnalyticsConnector, getAttributionTrackingConfig, @@ -24,6 +24,7 @@ import { } from '@amplitude/analytics-types'; import { convertProxyObjectToRealObject, isInstanceProxy } from './utils/snippet-helper'; import { Context } from './plugins/context'; +import { Destination } from './plugins/destination'; import { useBrowserConfig, createTransport } from './config'; import { webAttributionPlugin } from '@amplitude/plugin-web-attribution-browser'; import { pageViewTrackingPlugin } from '@amplitude/plugin-page-view-tracking-browser'; @@ -86,7 +87,7 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { // Step 4: Install plugins // Do not track any events before this - await this.add(new Destination()).promise; + await this.add(new Destination(this.config)).promise; await this.add(new Context()).promise; await this.add(new IdentityEventSender()).promise; diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index 9158c9519..c95a60bfd 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -77,7 +77,7 @@ export class BrowserConfig extends Config implements IBrowserConfig { public transport: 'fetch' | 'xhr' | 'beacon' = 'fetch', public useBatch: boolean = false, userId?: string, - public diagnosticProvider: DiagnosticPlugin = new Diagnostic(), + public diagnosticProvider: DiagnosticPlugin | undefined = new Diagnostic(), ) { super({ apiKey, storageProvider, transportProvider: createTransport(transport) }); this._cookieStorage = cookieStorage; diff --git a/packages/analytics-browser/src/plugins/destination.ts b/packages/analytics-browser/src/plugins/destination.ts index e0e811083..e789fa6f8 100644 --- a/packages/analytics-browser/src/plugins/destination.ts +++ b/packages/analytics-browser/src/plugins/destination.ts @@ -1,6 +1,5 @@ import { Destination as CoreDestination, buildResult } from '@amplitude/analytics-core'; -import { BrowserConfig } from '../../src/config'; -import { DestinationContext as Context } from '@amplitude/analytics-types'; +import { BrowserConfig, DestinationContext as Context } from '@amplitude/analytics-types'; export class Destination extends CoreDestination { // this.config is defined in setup() which will always be called first @@ -8,13 +7,13 @@ export class Destination extends CoreDestination { // @ts-ignore config: BrowserConfig; - async setup(config: BrowserConfig): Promise { + constructor(config: BrowserConfig) { + super(); this.config = config; - return super.setup(config); } async fulfillRequest(list: Context[], code: number, message: string) { - await this.config.diagnosticProvider.track(list.length, code, message); + await this.config.diagnosticProvider?.track(list.length, code, message); this.saveEvents(); list.forEach((context) => context.callback(buildResult(context.event, code, message))); } diff --git a/packages/analytics-browser/test/plugins/destination.test.ts b/packages/analytics-browser/test/plugins/destination.test.ts index 9fc5f7ea6..13ee6df74 100644 --- a/packages/analytics-browser/test/plugins/destination.test.ts +++ b/packages/analytics-browser/test/plugins/destination.test.ts @@ -7,27 +7,28 @@ describe('destination', () => { const apiKey = core.UUID(); const someDiagnosticProvider: core.Diagnostic = expect.any(core.Diagnostic) as core.Diagnostic; - describe('setup', () => { - test('should setup plugin', async () => { - const destination = new Destination(); + describe('constructor', () => { + test('should init plugin with config', async () => { const config = (await useBrowserConfig(apiKey, undefined, new AmplitudeBrowser())) as BrowserConfig; - await destination.setup(config); + const destination = new Destination(config); expect(destination.config.diagnosticProvider).toEqual(someDiagnosticProvider); }); }); describe('fulfillRequest', () => { - test('should track diagnostics', async () => { - const destination = new Destination(); + test.each([ + [true, 1], + [false, 0], + ])('should track diagnostics by default', async (isDiagnosticEnabled, diagnosticTrackTimes) => { + const config = (await useBrowserConfig(apiKey, undefined, new AmplitudeBrowser())) as BrowserConfig; const mockDiagnosticProvider = { type: 'destination' as const, execute: jest.fn(), flush: jest.fn(), track: jest.fn(), }; - const config = (await useBrowserConfig(apiKey, undefined, new AmplitudeBrowser())) as BrowserConfig; - config.diagnosticProvider = mockDiagnosticProvider; - destination.config = config; + config.diagnosticProvider = isDiagnosticEnabled ? mockDiagnosticProvider : undefined; + const destination = new Destination(config); const callback = jest.fn(); const context = { attempts: 0, @@ -40,7 +41,7 @@ describe('destination', () => { await destination.fulfillRequest([context], 200, 'success'); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith(core.buildResult(context.event, 200, 'success')); - expect(mockDiagnosticProvider.track).toHaveBeenCalledTimes(1); + expect(mockDiagnosticProvider.track).toHaveBeenCalledTimes(diagnosticTrackTimes); }); }); }); From cd04c90184de768560a163e485404b9c5c684b43 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Fri, 25 Aug 2023 17:05:39 -0700 Subject: [PATCH 04/50] chore: delete comments --- packages/analytics-browser/src/plugins/destination.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/analytics-browser/src/plugins/destination.ts b/packages/analytics-browser/src/plugins/destination.ts index e789fa6f8..b85649def 100644 --- a/packages/analytics-browser/src/plugins/destination.ts +++ b/packages/analytics-browser/src/plugins/destination.ts @@ -2,9 +2,6 @@ import { Destination as CoreDestination, buildResult } from '@amplitude/analytic import { BrowserConfig, DestinationContext as Context } from '@amplitude/analytics-types'; export class Destination extends CoreDestination { - // this.config is defined in setup() which will always be called first - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore config: BrowserConfig; constructor(config: BrowserConfig) { From f2e67b1214103b7ec7d0b8b21a8d47baab3c71c9 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Sun, 27 Aug 2023 22:48:32 -0700 Subject: [PATCH 05/50] test: add test for index --- packages/analytics-core/test/index.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/analytics-core/test/index.test.ts b/packages/analytics-core/test/index.test.ts index 96531d8fa..514a6d74e 100644 --- a/packages/analytics-core/test/index.test.ts +++ b/packages/analytics-core/test/index.test.ts @@ -14,6 +14,8 @@ import { UUID, MemoryStorage, createIdentifyEvent, + Diagnostic, + buildResult, } from '../src/index'; describe('index', () => { @@ -31,6 +33,7 @@ describe('index', () => { expect(typeof client.remove).toBe('function'); expect(typeof BaseTransport).toBe('function'); expect(typeof Destination).toBe('function'); + expect(typeof Diagnostic).toBe('function'); expect(typeof Config).toBe('function'); expect(typeof Logger).toBe('function'); expect(typeof returnWrapper).toBe('function'); @@ -40,6 +43,7 @@ describe('index', () => { expect(typeof UUID).toBe('function'); expect(typeof MemoryStorage).toBe('function'); expect(typeof createIdentifyEvent).toBe('function'); + expect(typeof buildResult).toBe('function'); expect(AMPLITUDE_PREFIX).toBe('AMP'); expect(STORAGE_PREFIX).toBe('AMP_unsent'); }); From a303868b75058c354fbe079f6f9dbd97506b8658 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Mon, 28 Aug 2023 00:26:35 -0700 Subject: [PATCH 06/50] fix: update diagnostic --- .../analytics-core/src/plugins/diagnostic.ts | 65 +++++++++++-------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/packages/analytics-core/src/plugins/diagnostic.ts b/packages/analytics-core/src/plugins/diagnostic.ts index 897b9f56e..f4e332825 100644 --- a/packages/analytics-core/src/plugins/diagnostic.ts +++ b/packages/analytics-core/src/plugins/diagnostic.ts @@ -1,34 +1,36 @@ -import { Config, CoreClient, DiagnosticPlugin, Event, Result } from '@amplitude/analytics-types'; +import { DiagnosticEvent, DiagnosticPlugin, Event, Result } from '@amplitude/analytics-types'; import fetch from 'node-fetch'; export class Diagnostic implements DiagnosticPlugin { name = '@amplitude/plugin-diagnostic'; type = 'destination' as const; + serverUrl = 'http://localhost:8000'; - constructor() { - // do something - } + queue: DiagnosticEvent[] = []; + scheduled: ReturnType | null = null; + delay = 60000; // deault delay is 1 minute - execute(_context: Event): Promise { - throw new Error('Method not implemented.'); - // it is called by timeline when client.track() - // do nothing when execute is called - } - flush?(): Promise { - throw new Error('Method not implemented.'); - // should flush all unsent events - } - setup?(_config: Config, _client: CoreClient): Promise { - throw new Error('Method not implemented.'); - } - teardown?(): Promise { - throw new Error('Method not implemented.'); - } async track(eventCount: number, code: number, message: string) { - // add event to queue - // https://github.com/amplitude/Amplitude-TypeScript/blob/502a080b6eca2bc390b5d8076f24b9137d213f89/packages/analytics-core/src/plugins/destination.ts#L70-L80 + this.queue.push(this.diagnosticEventBuilder(eventCount, code, message)); + + if (!this.scheduled) { + this.scheduled = setTimeout(() => { + void this.flush(); + }, this.delay); + } + } + + flush = async (): Promise => { + await fetch(this.serverUrl, this.requestPayloadBuilder(this.queue)); - const payload = { + if (this.scheduled) { + clearTimeout(this.scheduled); + this.scheduled = null; + } + }; + + diagnosticEventBuilder(eventCount: number, code: number, message: string): DiagnosticEvent { + return { time: Date.now(), event_properties: { response_error_code: code, @@ -38,18 +40,27 @@ export class Diagnostic implements DiagnosticPlugin { }, library: 'diagnostic-test-library', }; + } - const serverUrl = 'http://localhost:8000'; - - const body = { + requestPayloadBuilder(events: DiagnosticEvent[]): object { + return { headers: { 'Content-Type': 'application/json', Accept: '*/*', }, - body: JSON.stringify(payload), + events: events, method: 'POST', }; + } - await fetch(serverUrl, body); + execute(_context: Event): Promise { + return Promise.resolve({ + event: { event_type: 'diagnostic event' }, + code: -1, + message: 'this method should not be called, use track() instead', + }); + // this method is not implemented + // it's kept here to satisfy the interface + // track() should be used instead } } From 4521a9acd2164b692c34a0e4a03ad8c930a11cb1 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Mon, 28 Aug 2023 00:26:50 -0700 Subject: [PATCH 07/50] test: diagnostic --- .../test/plugins/diagnostic.test.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 packages/analytics-core/test/plugins/diagnostic.test.ts diff --git a/packages/analytics-core/test/plugins/diagnostic.test.ts b/packages/analytics-core/test/plugins/diagnostic.test.ts new file mode 100644 index 000000000..a176cf7ae --- /dev/null +++ b/packages/analytics-core/test/plugins/diagnostic.test.ts @@ -0,0 +1,60 @@ +import { Diagnostic } from '../../src/plugins/diagnostic'; + +jest.mock('node-fetch'); // Mocking node-fetch library +jest.useFakeTimers(); + +describe('Diagnostic', () => { + let diagnostic: Diagnostic; + const eventCount = 5; + const code = 200; + + beforeEach(() => { + diagnostic = new Diagnostic(); + }); + + afterEach(() => { + jest.restoreAllMocks(); // Restore mocked functions after each test + }); + + test('should add events to the queue when track method is called', async () => { + await diagnostic.track(eventCount, code, 'Test message'); + + expect(diagnostic.queue).toHaveLength(1); + expect(diagnostic.queue[0].event_properties.event_count).toBe(eventCount); + expect(diagnostic.queue[0].event_properties.response_error_code).toBe(code); + expect(diagnostic.queue[0].event_properties.trigger).toBe('Test message'); + expect(diagnostic.queue[0].library).toBe('diagnostic-test-library'); + }); + + test('should schedule flush when track is called for the first time 0', async () => { + const setTimeoutMock = jest.spyOn(global, 'setTimeout'); + + await diagnostic.track(eventCount, code, 'Test message'); + + jest.advanceTimersByTime(diagnostic.delay); + expect(setTimeoutMock).toHaveBeenCalledTimes(1); + expect(setTimeoutMock.mock.calls[0][0]).toBeInstanceOf(Function); + expect(setTimeoutMock.mock.calls[0][1]).toBe(diagnostic.delay); + setTimeoutMock.mockRestore(); + }); + + test('should clear scheduled timeout when flush is called', async () => { + const clearTimeoutMock = jest.spyOn(global, 'clearTimeout'); + const setTimeoutMock = jest.spyOn(global, 'setTimeout'); + + await diagnostic.track(eventCount, code, 'Scheduled timeout test'); + await diagnostic.flush(); + + expect(setTimeoutMock).toHaveBeenCalledTimes(1); + expect(clearTimeoutMock).toHaveBeenCalledTimes(1); + expect(diagnostic.scheduled).toBeNull(); + }); + + test('should not be called, use track() instead', async () => { + const context = { event_type: 'custom event' }; + + const result = await diagnostic.execute(context); + + expect(result.message).toBe('this method should not be called, use track() instead'); + }); +}); From 957ec26f45d9ae71cf45f7f79e1c3ef855c6b584 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Mon, 28 Aug 2023 09:44:14 -0700 Subject: [PATCH 08/50] fix: fetch --- .../src/plugins/diagnostic.ts | 8 ++ .../analytics-core/src/plugins/diagnostic.ts | 10 +-- .../test/plugins/diagnostic.test.ts | 89 +++++++++++++------ 3 files changed, 75 insertions(+), 32 deletions(-) create mode 100644 packages/analytics-browser/src/plugins/diagnostic.ts diff --git a/packages/analytics-browser/src/plugins/diagnostic.ts b/packages/analytics-browser/src/plugins/diagnostic.ts new file mode 100644 index 000000000..315799c93 --- /dev/null +++ b/packages/analytics-browser/src/plugins/diagnostic.ts @@ -0,0 +1,8 @@ +import { Diagnostic as CoreDiagnostic } from '@amplitude/analytics-core'; + +export class Diagnostic extends CoreDiagnostic { + async flush(): Promise { + await fetch(this.serverUrl, this.requestPayloadBuilder(this.queue)); + await super.flush(); + } +} diff --git a/packages/analytics-core/src/plugins/diagnostic.ts b/packages/analytics-core/src/plugins/diagnostic.ts index f4e332825..ac712d365 100644 --- a/packages/analytics-core/src/plugins/diagnostic.ts +++ b/packages/analytics-core/src/plugins/diagnostic.ts @@ -1,10 +1,9 @@ import { DiagnosticEvent, DiagnosticPlugin, Event, Result } from '@amplitude/analytics-types'; -import fetch from 'node-fetch'; export class Diagnostic implements DiagnosticPlugin { name = '@amplitude/plugin-diagnostic'; type = 'destination' as const; - serverUrl = 'http://localhost:8000'; + public serverUrl = new URL('http://localhost:8000'); queue: DiagnosticEvent[] = []; scheduled: ReturnType | null = null; @@ -20,14 +19,15 @@ export class Diagnostic implements DiagnosticPlugin { } } - flush = async (): Promise => { - await fetch(this.serverUrl, this.requestPayloadBuilder(this.queue)); + async flush(): Promise { + // send http request based on environment + // implemented in its child class if (this.scheduled) { clearTimeout(this.scheduled); this.scheduled = null; } - }; + } diagnosticEventBuilder(eventCount: number, code: number, message: string): DiagnosticEvent { return { diff --git a/packages/analytics-core/test/plugins/diagnostic.test.ts b/packages/analytics-core/test/plugins/diagnostic.test.ts index a176cf7ae..fe64cb3cb 100644 --- a/packages/analytics-core/test/plugins/diagnostic.test.ts +++ b/packages/analytics-core/test/plugins/diagnostic.test.ts @@ -1,3 +1,4 @@ +import { DiagnosticEvent } from '@amplitude/analytics-types'; import { Diagnostic } from '../../src/plugins/diagnostic'; jest.mock('node-fetch'); // Mocking node-fetch library @@ -16,45 +17,79 @@ describe('Diagnostic', () => { jest.restoreAllMocks(); // Restore mocked functions after each test }); - test('should add events to the queue when track method is called', async () => { - await diagnostic.track(eventCount, code, 'Test message'); + describe('track', () => { + test('should add events to the queue when track method is called', async () => { + await diagnostic.track(eventCount, code, 'Test message'); - expect(diagnostic.queue).toHaveLength(1); - expect(diagnostic.queue[0].event_properties.event_count).toBe(eventCount); - expect(diagnostic.queue[0].event_properties.response_error_code).toBe(code); - expect(diagnostic.queue[0].event_properties.trigger).toBe('Test message'); - expect(diagnostic.queue[0].library).toBe('diagnostic-test-library'); + expect(diagnostic.queue).toHaveLength(1); + expect(diagnostic.queue[0].event_properties.event_count).toBe(eventCount); + expect(diagnostic.queue[0].event_properties.response_error_code).toBe(code); + expect(diagnostic.queue[0].event_properties.trigger).toBe('Test message'); + expect(diagnostic.queue[0].library).toBe('diagnostic-test-library'); + }); + + test('should schedule flush when track is called for the first time 0', async () => { + const setTimeoutMock = jest.spyOn(global, 'setTimeout'); + + await diagnostic.track(eventCount, code, 'Test message'); + + jest.advanceTimersByTime(diagnostic.delay); + expect(setTimeoutMock).toHaveBeenCalledTimes(1); + expect(setTimeoutMock.mock.calls[0][0]).toBeInstanceOf(Function); + expect(setTimeoutMock.mock.calls[0][1]).toBe(diagnostic.delay); + setTimeoutMock.mockRestore(); + }); }); - test('should schedule flush when track is called for the first time 0', async () => { - const setTimeoutMock = jest.spyOn(global, 'setTimeout'); + describe('flush', () => { + test('should clear scheduled timeout when flush is called', async () => { + const clearTimeoutMock = jest.spyOn(global, 'clearTimeout'); + const setTimeoutMock = jest.spyOn(global, 'setTimeout'); - await diagnostic.track(eventCount, code, 'Test message'); + await diagnostic.track(eventCount, code, 'Scheduled timeout test'); + await diagnostic.flush(); - jest.advanceTimersByTime(diagnostic.delay); - expect(setTimeoutMock).toHaveBeenCalledTimes(1); - expect(setTimeoutMock.mock.calls[0][0]).toBeInstanceOf(Function); - expect(setTimeoutMock.mock.calls[0][1]).toBe(diagnostic.delay); - setTimeoutMock.mockRestore(); + expect(setTimeoutMock).toHaveBeenCalledTimes(1); + expect(clearTimeoutMock).toHaveBeenCalledTimes(1); + expect(diagnostic.scheduled).toBeNull(); + }); }); - test('should clear scheduled timeout when flush is called', async () => { - const clearTimeoutMock = jest.spyOn(global, 'clearTimeout'); - const setTimeoutMock = jest.spyOn(global, 'setTimeout'); + describe('requestPayloadBuilder', () => { + test('should return correct payload', () => { + const events: DiagnosticEvent[] = [ + { + time: Date.now(), + event_properties: { + response_error_code: code, + trigger: 'test trigger', + action: 'test action', + event_count: eventCount, + }, + library: 'diagnostic-test-library', + }, + ]; - await diagnostic.track(eventCount, code, 'Scheduled timeout test'); - await diagnostic.flush(); + const expectedPayload = { + headers: { + 'Content-Type': 'application/json', + Accept: '*/*', + }, + events: events, + method: 'POST', + }; - expect(setTimeoutMock).toHaveBeenCalledTimes(1); - expect(clearTimeoutMock).toHaveBeenCalledTimes(1); - expect(diagnostic.scheduled).toBeNull(); + expect(diagnostic.requestPayloadBuilder(events)).toEqual(expectedPayload); + }); }); - test('should not be called, use track() instead', async () => { - const context = { event_type: 'custom event' }; + describe('execute', () => { + test('should not be called, use track() instead', async () => { + const context = { event_type: 'custom event' }; - const result = await diagnostic.execute(context); + const result = await diagnostic.execute(context); - expect(result.message).toBe('this method should not be called, use track() instead'); + expect(result.message).toBe('this method should not be called, use track() instead'); + }); }); }); From 922f349482e9b151274b21ac8d1b044b2e1680fb Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Mon, 28 Aug 2023 10:01:28 -0700 Subject: [PATCH 09/50] test: browser diagnostic --- packages/analytics-browser/src/config.ts | 3 ++- packages/analytics-browser/test/config.test.ts | 3 ++- .../test/plugins/diagnostic.test.ts | 15 +++++++++++++++ yarn.lock | 17 ----------------- 4 files changed, 19 insertions(+), 19 deletions(-) create mode 100644 packages/analytics-browser/test/plugins/diagnostic.test.ts diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index c95a60bfd..107807115 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -15,7 +15,7 @@ import { ServerZoneType, DiagnosticPlugin, } from '@amplitude/analytics-types'; -import { Config, Diagnostic, Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; +import { Config, Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; import { CookieStorage, getCookieName, FetchTransport, getQueryParams } from '@amplitude/analytics-client-common'; import { LocalStorage } from './storage/local-storage'; @@ -26,6 +26,7 @@ import { parseLegacyCookies } from './cookie-migration'; import { CookieOptions } from '@amplitude/analytics-types/lib/esm/config/browser'; import { DEFAULT_IDENTITY_STORAGE, DEFAULT_SERVER_ZONE } from './constants'; import { AmplitudeBrowser } from './browser-client'; +import { Diagnostic } from './plugins/diagnostic'; // Exported for testing purposes only. Do not expose to public interface. export class BrowserConfig extends Config implements IBrowserConfig { diff --git a/packages/analytics-browser/test/config.test.ts b/packages/analytics-browser/test/config.test.ts index c32caf2b5..33e679777 100644 --- a/packages/analytics-browser/test/config.test.ts +++ b/packages/analytics-browser/test/config.test.ts @@ -11,6 +11,7 @@ import { SendBeaconTransport } from '../src/transports/send-beacon'; import { uuidPattern } from './helpers/constants'; import { DEFAULT_IDENTITY_STORAGE, DEFAULT_SERVER_ZONE } from '../src/constants'; import { AmplitudeBrowser } from '../src/browser-client'; +import { Diagnostic } from '../src/plugins/diagnostic'; describe('config', () => { const someUUID: string = expect.stringMatching(uuidPattern) as string; @@ -20,7 +21,7 @@ describe('config', () => { const someLocalStorage: LocalStorageModule.LocalStorage = expect.any( LocalStorageModule.LocalStorage, ) as LocalStorageModule.LocalStorage; - const someDiagnosticProvider: core.Diagnostic = expect.any(core.Diagnostic) as core.Diagnostic; + const someDiagnosticProvider: Diagnostic = expect.any(Diagnostic) as Diagnostic; let apiKey = ''; diff --git a/packages/analytics-browser/test/plugins/diagnostic.test.ts b/packages/analytics-browser/test/plugins/diagnostic.test.ts new file mode 100644 index 000000000..5aebd896c --- /dev/null +++ b/packages/analytics-browser/test/plugins/diagnostic.test.ts @@ -0,0 +1,15 @@ +import { Diagnostic } from '../../src/plugins/diagnostic'; + +describe('Diagnostic', () => { + test('should fetch', async () => { + const diagnostic = new Diagnostic(); + const fetchMock = jest.fn().mockResolvedValueOnce({} as Response); + global.fetch = fetchMock; + + await diagnostic.track(5, 200, 'Test message'); + await diagnostic.flush(); + + expect(fetchMock).toHaveBeenCalledTimes(1); + fetchMock.mockRestore(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 320860c39..2be390f76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3950,14 +3950,6 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== -"@types/node-fetch@^2.6.4": - version "2.6.4" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660" - integrity sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg== - dependencies: - "@types/node" "*" - form-data "^3.0.0" - "@types/node@*", "@types/node@^18.11.14": version "18.11.14" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.14.tgz#a8571b25f3a31e9ded14e3ab9488509adef831d8" @@ -6836,15 +6828,6 @@ for-in@^1.0.2: resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== -form-data@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" - integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" From 21dc41b1b5762952393d10c4c8c3d7a035344bdf Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Mon, 28 Aug 2023 10:06:41 -0700 Subject: [PATCH 10/50] test: delete node fetch --- packages/analytics-core/test/plugins/diagnostic.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/analytics-core/test/plugins/diagnostic.test.ts b/packages/analytics-core/test/plugins/diagnostic.test.ts index fe64cb3cb..9ab36233c 100644 --- a/packages/analytics-core/test/plugins/diagnostic.test.ts +++ b/packages/analytics-core/test/plugins/diagnostic.test.ts @@ -1,7 +1,6 @@ import { DiagnosticEvent } from '@amplitude/analytics-types'; import { Diagnostic } from '../../src/plugins/diagnostic'; -jest.mock('node-fetch'); // Mocking node-fetch library jest.useFakeTimers(); describe('Diagnostic', () => { From c0ba9114944febccb1592dd457453659580fe3eb Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 6 Sep 2023 10:49:08 -0700 Subject: [PATCH 11/50] fix: diagnosticProvider should be in core config test: diagnostic --- .../analytics-browser/src/browser-client.ts | 5 +- packages/analytics-browser/src/config.ts | 7 ++- .../src/plugins/destination.ts | 17 ------- .../analytics-browser/test/config.test.ts | 3 +- .../test/plugins/destination.test.ts | 47 ------------------- packages/analytics-core/src/config.ts | 2 + .../src/{plugins => }/diagnostic.ts | 8 ++-- packages/analytics-core/src/index.ts | 2 +- .../analytics-core/src/plugins/destination.ts | 1 + .../test/{plugins => }/diagnostic.test.ts | 2 +- .../test/plugins/destination.test.ts | 42 +++++++++++++++++ .../analytics-types/src/config/browser.ts | 2 - packages/analytics-types/src/config/core.ts | 2 + packages/analytics-types/src/diagnostic.ts | 3 ++ packages/analytics-types/src/index.ts | 3 +- packages/analytics-types/src/plugin.ts | 10 +--- 16 files changed, 64 insertions(+), 92 deletions(-) delete mode 100644 packages/analytics-browser/src/plugins/destination.ts delete mode 100644 packages/analytics-browser/test/plugins/destination.test.ts rename packages/analytics-core/src/{plugins => }/diagnostic.ts (84%) rename packages/analytics-core/test/{plugins => }/diagnostic.test.ts (98%) create mode 100644 packages/analytics-types/src/diagnostic.ts diff --git a/packages/analytics-browser/src/browser-client.ts b/packages/analytics-browser/src/browser-client.ts index 3bf0aed68..8c7c2b3c0 100644 --- a/packages/analytics-browser/src/browser-client.ts +++ b/packages/analytics-browser/src/browser-client.ts @@ -1,4 +1,4 @@ -import { AmplitudeCore, Identify, returnWrapper, Revenue, UUID } from '@amplitude/analytics-core'; +import { AmplitudeCore, Destination, Identify, returnWrapper, Revenue, UUID } from '@amplitude/analytics-core'; import { getAnalyticsConnector, getAttributionTrackingConfig, @@ -24,7 +24,6 @@ import { } from '@amplitude/analytics-types'; import { convertProxyObjectToRealObject, isInstanceProxy } from './utils/snippet-helper'; import { Context } from './plugins/context'; -import { Destination } from './plugins/destination'; import { useBrowserConfig, createTransport } from './config'; import { webAttributionPlugin } from '@amplitude/plugin-web-attribution-browser'; import { pageViewTrackingPlugin } from '@amplitude/plugin-page-view-tracking-browser'; @@ -87,7 +86,7 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient { // Step 4: Install plugins // Do not track any events before this - await this.add(new Destination(this.config)).promise; + await this.add(new Destination()).promise; await this.add(new Context()).promise; await this.add(new IdentityEventSender()).promise; diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index 107807115..d888f4fb4 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -3,6 +3,7 @@ import { BrowserOptions, BrowserConfig as IBrowserConfig, DefaultTrackingOptions, + Diagnostic as IDiagnostic, Storage, TrackingOptions, TransportType, @@ -13,9 +14,8 @@ import { IngestionMetadata, IdentityStorageType, ServerZoneType, - DiagnosticPlugin, } from '@amplitude/analytics-types'; -import { Config, Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; +import { Config, Diagnostic, Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; import { CookieStorage, getCookieName, FetchTransport, getQueryParams } from '@amplitude/analytics-client-common'; import { LocalStorage } from './storage/local-storage'; @@ -26,7 +26,6 @@ import { parseLegacyCookies } from './cookie-migration'; import { CookieOptions } from '@amplitude/analytics-types/lib/esm/config/browser'; import { DEFAULT_IDENTITY_STORAGE, DEFAULT_SERVER_ZONE } from './constants'; import { AmplitudeBrowser } from './browser-client'; -import { Diagnostic } from './plugins/diagnostic'; // Exported for testing purposes only. Do not expose to public interface. export class BrowserConfig extends Config implements IBrowserConfig { @@ -78,7 +77,7 @@ export class BrowserConfig extends Config implements IBrowserConfig { public transport: 'fetch' | 'xhr' | 'beacon' = 'fetch', public useBatch: boolean = false, userId?: string, - public diagnosticProvider: DiagnosticPlugin | undefined = new Diagnostic(), + public diagnosticProvider: IDiagnostic | undefined = new Diagnostic(), ) { super({ apiKey, storageProvider, transportProvider: createTransport(transport) }); this._cookieStorage = cookieStorage; diff --git a/packages/analytics-browser/src/plugins/destination.ts b/packages/analytics-browser/src/plugins/destination.ts deleted file mode 100644 index b85649def..000000000 --- a/packages/analytics-browser/src/plugins/destination.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Destination as CoreDestination, buildResult } from '@amplitude/analytics-core'; -import { BrowserConfig, DestinationContext as Context } from '@amplitude/analytics-types'; - -export class Destination extends CoreDestination { - config: BrowserConfig; - - constructor(config: BrowserConfig) { - super(); - this.config = config; - } - - async fulfillRequest(list: Context[], code: number, message: string) { - await this.config.diagnosticProvider?.track(list.length, code, message); - this.saveEvents(); - list.forEach((context) => context.callback(buildResult(context.event, code, message))); - } -} diff --git a/packages/analytics-browser/test/config.test.ts b/packages/analytics-browser/test/config.test.ts index 33e679777..c32caf2b5 100644 --- a/packages/analytics-browser/test/config.test.ts +++ b/packages/analytics-browser/test/config.test.ts @@ -11,7 +11,6 @@ import { SendBeaconTransport } from '../src/transports/send-beacon'; import { uuidPattern } from './helpers/constants'; import { DEFAULT_IDENTITY_STORAGE, DEFAULT_SERVER_ZONE } from '../src/constants'; import { AmplitudeBrowser } from '../src/browser-client'; -import { Diagnostic } from '../src/plugins/diagnostic'; describe('config', () => { const someUUID: string = expect.stringMatching(uuidPattern) as string; @@ -21,7 +20,7 @@ describe('config', () => { const someLocalStorage: LocalStorageModule.LocalStorage = expect.any( LocalStorageModule.LocalStorage, ) as LocalStorageModule.LocalStorage; - const someDiagnosticProvider: Diagnostic = expect.any(Diagnostic) as Diagnostic; + const someDiagnosticProvider: core.Diagnostic = expect.any(core.Diagnostic) as core.Diagnostic; let apiKey = ''; diff --git a/packages/analytics-browser/test/plugins/destination.test.ts b/packages/analytics-browser/test/plugins/destination.test.ts deleted file mode 100644 index 13ee6df74..000000000 --- a/packages/analytics-browser/test/plugins/destination.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Destination } from '../../src/plugins/destination'; -import { BrowserConfig, useBrowserConfig } from '../../src/config'; -import * as core from '@amplitude/analytics-core'; -import { AmplitudeBrowser } from '../../src/browser-client'; - -describe('destination', () => { - const apiKey = core.UUID(); - const someDiagnosticProvider: core.Diagnostic = expect.any(core.Diagnostic) as core.Diagnostic; - - describe('constructor', () => { - test('should init plugin with config', async () => { - const config = (await useBrowserConfig(apiKey, undefined, new AmplitudeBrowser())) as BrowserConfig; - const destination = new Destination(config); - expect(destination.config.diagnosticProvider).toEqual(someDiagnosticProvider); - }); - }); - - describe('fulfillRequest', () => { - test.each([ - [true, 1], - [false, 0], - ])('should track diagnostics by default', async (isDiagnosticEnabled, diagnosticTrackTimes) => { - const config = (await useBrowserConfig(apiKey, undefined, new AmplitudeBrowser())) as BrowserConfig; - const mockDiagnosticProvider = { - type: 'destination' as const, - execute: jest.fn(), - flush: jest.fn(), - track: jest.fn(), - }; - config.diagnosticProvider = isDiagnosticEnabled ? mockDiagnosticProvider : undefined; - const destination = new Destination(config); - const callback = jest.fn(); - const context = { - attempts: 0, - callback, - event: { - event_type: 'event_type', - }, - timeout: 0, - }; - await destination.fulfillRequest([context], 200, 'success'); - expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith(core.buildResult(context.event, 200, 'success')); - expect(mockDiagnosticProvider.track).toHaveBeenCalledTimes(diagnosticTrackTimes); - }); - }); -}); diff --git a/packages/analytics-core/src/config.ts b/packages/analytics-core/src/config.ts index bb6ee4081..5e0f681b1 100644 --- a/packages/analytics-core/src/config.ts +++ b/packages/analytics-core/src/config.ts @@ -1,6 +1,7 @@ import { Event, Config as IConfig, + Diagnostic as IDiagnostic, Logger as ILogger, LogLevel, Storage, @@ -48,6 +49,7 @@ export class Config implements IConfig { transportProvider: Transport; storageProvider?: Storage; useBatch: boolean; + diagnosticProvider?: IDiagnostic; protected _optOut = false; get optOut() { diff --git a/packages/analytics-core/src/plugins/diagnostic.ts b/packages/analytics-core/src/diagnostic.ts similarity index 84% rename from packages/analytics-core/src/plugins/diagnostic.ts rename to packages/analytics-core/src/diagnostic.ts index ac712d365..4930977b6 100644 --- a/packages/analytics-core/src/plugins/diagnostic.ts +++ b/packages/analytics-core/src/diagnostic.ts @@ -1,15 +1,13 @@ -import { DiagnosticEvent, DiagnosticPlugin, Event, Result } from '@amplitude/analytics-types'; +import { DiagnosticEvent, Diagnostic as IDiagnostic, Event, Result } from '@amplitude/analytics-types'; -export class Diagnostic implements DiagnosticPlugin { - name = '@amplitude/plugin-diagnostic'; - type = 'destination' as const; +export class Diagnostic implements IDiagnostic { public serverUrl = new URL('http://localhost:8000'); queue: DiagnosticEvent[] = []; scheduled: ReturnType | null = null; delay = 60000; // deault delay is 1 minute - async track(eventCount: number, code: number, message: string) { + track(eventCount: number, code: number, message: string) { this.queue.push(this.diagnosticEventBuilder(eventCount, code, message)); if (!this.scheduled) { diff --git a/packages/analytics-core/src/index.ts b/packages/analytics-core/src/index.ts index 7f4e8a156..a17d893ae 100644 --- a/packages/analytics-core/src/index.ts +++ b/packages/analytics-core/src/index.ts @@ -2,7 +2,7 @@ export { AmplitudeCore } from './core-client'; export { Identify } from './identify'; export { Revenue } from './revenue'; export { Destination } from './plugins/destination'; -export { Diagnostic } from './plugins/diagnostic'; +export { Diagnostic } from './diagnostic'; export { Config } from './config'; export { Logger } from './logger'; export { AMPLITUDE_PREFIX, STORAGE_PREFIX } from './constants'; diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index 78b4a0251..a50e9efb3 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -288,6 +288,7 @@ export class Destination implements DestinationPlugin { } fulfillRequest(list: Context[], code: number, message: string) { + this.config.diagnosticProvider?.track(list.length, code, message); this.saveEvents(); list.forEach((context) => context.callback(buildResult(context.event, code, message))); } diff --git a/packages/analytics-core/test/plugins/diagnostic.test.ts b/packages/analytics-core/test/diagnostic.test.ts similarity index 98% rename from packages/analytics-core/test/plugins/diagnostic.test.ts rename to packages/analytics-core/test/diagnostic.test.ts index 9ab36233c..43b650b7c 100644 --- a/packages/analytics-core/test/plugins/diagnostic.test.ts +++ b/packages/analytics-core/test/diagnostic.test.ts @@ -1,5 +1,5 @@ import { DiagnosticEvent } from '@amplitude/analytics-types'; -import { Diagnostic } from '../../src/plugins/diagnostic'; +import { Diagnostic } from '../src/diagnostic'; jest.useFakeTimers(); diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index 6ccab58f0..2cb7a1cea 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -1079,4 +1079,46 @@ describe('destination', () => { expect(result).toBe(''); }); }); + + describe('fulfillRequest', () => { + test('disgnostic should track if it enabled', () => { + const context = { + event: { + event_type: 'event_type', + }, + callback: () => undefined, + attempts: 0, + timeout: 0, + }; + const destination = new Destination(); + destination.config = useDefaultConfig(); + const diagnostic = { + track: () => undefined, + }; + destination.config.diagnosticProvider = diagnostic; + const diagnosticTrack = jest.spyOn(diagnostic, 'track'); + destination.fulfillRequest([context], 0, 'test_message'); + expect(diagnosticTrack).toHaveBeenCalledTimes(1); + }); + + test('disgnostic should not track if it disabled', () => { + const context = { + event: { + event_type: 'event_type', + }, + callback: () => undefined, + attempts: 0, + timeout: 0, + }; + const destination = new Destination(); + destination.config = useDefaultConfig(); + const diagnostic = { + track: () => undefined, + }; + destination.config.diagnosticProvider = undefined; + const diagnosticTrack = jest.spyOn(diagnostic, 'track'); + destination.fulfillRequest([context], 0, 'test_message'); + expect(diagnosticTrack).toHaveBeenCalledTimes(0); + }); + }); }); diff --git a/packages/analytics-types/src/config/browser.ts b/packages/analytics-types/src/config/browser.ts index 0374ab54e..0ce182be1 100644 --- a/packages/analytics-types/src/config/browser.ts +++ b/packages/analytics-types/src/config/browser.ts @@ -3,7 +3,6 @@ import { IdentityStorageType, Storage } from '../storage'; import { Transport } from '../transport'; import { Config } from './core'; import { PageTrackingOptions } from '../page-view-tracking'; -import { DiagnosticPlugin } from '../plugin'; export interface BrowserConfig extends ExternalBrowserConfig, InternalBrowserConfig {} @@ -19,7 +18,6 @@ export interface ExternalBrowserConfig extends Config { trackingOptions: TrackingOptions; transport?: 'fetch' | 'xhr' | 'beacon'; userId?: string; - diagnosticProvider?: DiagnosticPlugin; } interface InternalBrowserConfig { diff --git a/packages/analytics-types/src/config/core.ts b/packages/analytics-types/src/config/core.ts index 71c6b086d..c01cbcc07 100644 --- a/packages/analytics-types/src/config/core.ts +++ b/packages/analytics-types/src/config/core.ts @@ -5,6 +5,7 @@ import { ServerZoneType } from '../server-zone'; import { Storage } from '../storage'; import { Transport } from '../transport'; import { Logger, LogLevel } from '../logger'; +import { Diagnostic } from '../diagnostic'; export interface Config { apiKey: string; @@ -23,6 +24,7 @@ export interface Config { storageProvider?: Storage; transportProvider: Transport; useBatch: boolean; + diagnosticProvider?: Diagnostic; } export interface Options extends Partial { diff --git a/packages/analytics-types/src/diagnostic.ts b/packages/analytics-types/src/diagnostic.ts new file mode 100644 index 000000000..5004319a3 --- /dev/null +++ b/packages/analytics-types/src/diagnostic.ts @@ -0,0 +1,3 @@ +export interface Diagnostic { + track(eventCount: number, code: number, message: string): void; +} diff --git a/packages/analytics-types/src/index.ts b/packages/analytics-types/src/index.ts index 66e07a4dd..2745134fd 100644 --- a/packages/analytics-types/src/index.ts +++ b/packages/analytics-types/src/index.ts @@ -49,7 +49,8 @@ export { Logger, LogLevel, LogConfig, DebugContext } from './logger'; export { Payload } from './payload'; export { Plan } from './plan'; export { IngestionMetadata } from './ingestion-metadata'; -export { Plugin, BeforePlugin, EnrichmentPlugin, DestinationPlugin, DiagnosticPlugin, PluginType } from './plugin'; +export { Plugin, BeforePlugin, EnrichmentPlugin, DestinationPlugin, PluginType } from './plugin'; +export { Diagnostic } from './diagnostic'; export { Result } from './result'; export { Response, SuccessResponse, InvalidResponse, PayloadTooLargeResponse, RateLimitResponse } from './response'; export { QueueProxy, InstanceProxy } from './proxy'; diff --git a/packages/analytics-types/src/plugin.ts b/packages/analytics-types/src/plugin.ts index aa73712bc..ac7b6e1fd 100644 --- a/packages/analytics-types/src/plugin.ts +++ b/packages/analytics-types/src/plugin.ts @@ -31,12 +31,4 @@ export interface DestinationPlugin extends PluginBas flush?(): Promise; } -export interface DiagnosticPlugin extends DestinationPlugin { - track(eventCount: number, code: number, message: string): Promise; -} - -export type Plugin = - | BeforePlugin - | EnrichmentPlugin - | DestinationPlugin - | DiagnosticPlugin; +export type Plugin = BeforePlugin | EnrichmentPlugin | DestinationPlugin; From 470d82bc002a7f1057f158aa8e71ae5217edc2c4 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 6 Sep 2023 16:24:01 -0700 Subject: [PATCH 12/50] fix: node reference error --- packages/analytics-core/src/diagnostic.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/analytics-core/src/diagnostic.ts b/packages/analytics-core/src/diagnostic.ts index 4930977b6..c16790743 100644 --- a/packages/analytics-core/src/diagnostic.ts +++ b/packages/analytics-core/src/diagnostic.ts @@ -1,10 +1,10 @@ import { DiagnosticEvent, Diagnostic as IDiagnostic, Event, Result } from '@amplitude/analytics-types'; export class Diagnostic implements IDiagnostic { - public serverUrl = new URL('http://localhost:8000'); + public serverUrl = 'http://localhost:8000'; queue: DiagnosticEvent[] = []; - scheduled: ReturnType | null = null; + private scheduled: ReturnType | null = null; delay = 60000; // deault delay is 1 minute track(eventCount: number, code: number, message: string) { From f6796b53bd9d68fe8c74fd2976f75a8f06d515b7 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 6 Sep 2023 16:32:30 -0700 Subject: [PATCH 13/50] test: update diagnostic tests --- packages/analytics-core/test/diagnostic.test.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/analytics-core/test/diagnostic.test.ts b/packages/analytics-core/test/diagnostic.test.ts index 43b650b7c..61ad76a59 100644 --- a/packages/analytics-core/test/diagnostic.test.ts +++ b/packages/analytics-core/test/diagnostic.test.ts @@ -17,8 +17,8 @@ describe('Diagnostic', () => { }); describe('track', () => { - test('should add events to the queue when track method is called', async () => { - await diagnostic.track(eventCount, code, 'Test message'); + test('should add events to the queue when track method is called', () => { + diagnostic.track(eventCount, code, 'Test message'); expect(diagnostic.queue).toHaveLength(1); expect(diagnostic.queue[0].event_properties.event_count).toBe(eventCount); @@ -27,10 +27,10 @@ describe('Diagnostic', () => { expect(diagnostic.queue[0].library).toBe('diagnostic-test-library'); }); - test('should schedule flush when track is called for the first time 0', async () => { + test('should schedule flush when track is called for the first time 0', () => { const setTimeoutMock = jest.spyOn(global, 'setTimeout'); - await diagnostic.track(eventCount, code, 'Test message'); + diagnostic.track(eventCount, code, 'Test message'); jest.advanceTimersByTime(diagnostic.delay); expect(setTimeoutMock).toHaveBeenCalledTimes(1); @@ -45,12 +45,11 @@ describe('Diagnostic', () => { const clearTimeoutMock = jest.spyOn(global, 'clearTimeout'); const setTimeoutMock = jest.spyOn(global, 'setTimeout'); - await diagnostic.track(eventCount, code, 'Scheduled timeout test'); + diagnostic.track(eventCount, code, 'Scheduled timeout test'); await diagnostic.flush(); expect(setTimeoutMock).toHaveBeenCalledTimes(1); expect(clearTimeoutMock).toHaveBeenCalledTimes(1); - expect(diagnostic.scheduled).toBeNull(); }); }); From 4fb3fca7dc7156f6b01267bec9a37f096a55a0df Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 6 Sep 2023 16:47:44 -0700 Subject: [PATCH 14/50] fix: not export diagnostic event --- packages/analytics-core/src/diagnostic.ts | 13 ++++++++++++- packages/analytics-core/test/diagnostic.test.ts | 3 +-- packages/analytics-types/src/index.ts | 1 - 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/analytics-core/src/diagnostic.ts b/packages/analytics-core/src/diagnostic.ts index c16790743..4ef815b0e 100644 --- a/packages/analytics-core/src/diagnostic.ts +++ b/packages/analytics-core/src/diagnostic.ts @@ -1,4 +1,15 @@ -import { DiagnosticEvent, Diagnostic as IDiagnostic, Event, Result } from '@amplitude/analytics-types'; +import { Diagnostic as IDiagnostic, Event, Result } from '@amplitude/analytics-types'; + +interface DiagnosticEvent { + time: number; + event_properties: { + response_error_code: number; + trigger: string; + action: string; + event_count: number; + }; + library: string; +} export class Diagnostic implements IDiagnostic { public serverUrl = 'http://localhost:8000'; diff --git a/packages/analytics-core/test/diagnostic.test.ts b/packages/analytics-core/test/diagnostic.test.ts index 61ad76a59..de4cb24b2 100644 --- a/packages/analytics-core/test/diagnostic.test.ts +++ b/packages/analytics-core/test/diagnostic.test.ts @@ -1,4 +1,3 @@ -import { DiagnosticEvent } from '@amplitude/analytics-types'; import { Diagnostic } from '../src/diagnostic'; jest.useFakeTimers(); @@ -55,7 +54,7 @@ describe('Diagnostic', () => { describe('requestPayloadBuilder', () => { test('should return correct payload', () => { - const events: DiagnosticEvent[] = [ + const events = [ { time: Date.now(), event_properties: { diff --git a/packages/analytics-types/src/index.ts b/packages/analytics-types/src/index.ts index 2745134fd..e3618a0da 100644 --- a/packages/analytics-types/src/index.ts +++ b/packages/analytics-types/src/index.ts @@ -1,6 +1,5 @@ export { AmplitudeReturn } from './amplitude-promise'; export { BaseEvent, EventOptions } from './base-event'; -export { DiagnosticEvent } from './diagnostic-event'; export { Campaign, CampaignParser, From 7d15a8e3b0293767872398b61fc1eb156afcccc5 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 6 Sep 2023 16:48:57 -0700 Subject: [PATCH 15/50] fix: not export diagnostic event --- packages/analytics-types/src/diagnostic-event.ts | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 packages/analytics-types/src/diagnostic-event.ts diff --git a/packages/analytics-types/src/diagnostic-event.ts b/packages/analytics-types/src/diagnostic-event.ts deleted file mode 100644 index 89f1da9a7..000000000 --- a/packages/analytics-types/src/diagnostic-event.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface DiagnosticEvent { - time: number; - event_properties: { - response_error_code: number; - trigger: string; - action: string; - event_count: number; - }; - library: string; -} From 5121480a00fb7ba7f9af59b93b57356956b2b2ed Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 6 Sep 2023 23:05:12 -0700 Subject: [PATCH 16/50] fix: add diagnostic constructor --- packages/analytics-core/src/constants.ts | 1 + packages/analytics-core/src/diagnostic.ts | 13 ++++++++++--- .../analytics-core/test/diagnostic.test.ts | 19 +++++++++++++++++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/analytics-core/src/constants.ts b/packages/analytics-core/src/constants.ts index 4459374b1..e8f66d849 100644 --- a/packages/analytics-core/src/constants.ts +++ b/packages/analytics-core/src/constants.ts @@ -5,3 +5,4 @@ export const AMPLITUDE_SERVER_URL = 'https://api2.amplitude.com/2/httpapi'; export const EU_AMPLITUDE_SERVER_URL = 'https://api.eu.amplitude.com/2/httpapi'; export const AMPLITUDE_BATCH_SERVER_URL = 'https://api2.amplitude.com/batch'; export const EU_AMPLITUDE_BATCH_SERVER_URL = 'https://api.eu.amplitude.com/batch'; +export const DIAGNOSTIC_ENDPOINT = 'http://localhost:8000'; diff --git a/packages/analytics-core/src/diagnostic.ts b/packages/analytics-core/src/diagnostic.ts index 4ef815b0e..32099a0a5 100644 --- a/packages/analytics-core/src/diagnostic.ts +++ b/packages/analytics-core/src/diagnostic.ts @@ -1,4 +1,5 @@ import { Diagnostic as IDiagnostic, Event, Result } from '@amplitude/analytics-types'; +import { DIAGNOSTIC_ENDPOINT } from './constants'; interface DiagnosticEvent { time: number; @@ -12,11 +13,17 @@ interface DiagnosticEvent { } export class Diagnostic implements IDiagnostic { - public serverUrl = 'http://localhost:8000'; - + serverUrl: string; queue: DiagnosticEvent[] = []; + private scheduled: ReturnType | null = null; - delay = 60000; // deault delay is 1 minute + // deault delay is 1 minute + // make it private to prevent users from changing it to smaller value + private delay = 60000; + + constructor(serverUrl?: string) { + this.serverUrl = serverUrl ? serverUrl : DIAGNOSTIC_ENDPOINT; + } track(eventCount: number, code: number, message: string) { this.queue.push(this.diagnosticEventBuilder(eventCount, code, message)); diff --git a/packages/analytics-core/test/diagnostic.test.ts b/packages/analytics-core/test/diagnostic.test.ts index de4cb24b2..59bbeabaa 100644 --- a/packages/analytics-core/test/diagnostic.test.ts +++ b/packages/analytics-core/test/diagnostic.test.ts @@ -1,3 +1,4 @@ +import { DIAGNOSTIC_ENDPOINT } from '../src/constants'; import { Diagnostic } from '../src/diagnostic'; jest.useFakeTimers(); @@ -6,6 +7,7 @@ describe('Diagnostic', () => { let diagnostic: Diagnostic; const eventCount = 5; const code = 200; + const delay = 60000; beforeEach(() => { diagnostic = new Diagnostic(); @@ -15,6 +17,19 @@ describe('Diagnostic', () => { jest.restoreAllMocks(); // Restore mocked functions after each test }); + describe('constructor', () => { + test('should set serverUrl to default value if not provided', () => { + expect(diagnostic.serverUrl).toBe(DIAGNOSTIC_ENDPOINT); + }); + + test('should set serverUrl to provided value', () => { + const serverUrl = 'https://test.com'; + diagnostic = new Diagnostic(serverUrl); + + expect(diagnostic.serverUrl).toBe(serverUrl); + }); + }); + describe('track', () => { test('should add events to the queue when track method is called', () => { diagnostic.track(eventCount, code, 'Test message'); @@ -31,10 +46,10 @@ describe('Diagnostic', () => { diagnostic.track(eventCount, code, 'Test message'); - jest.advanceTimersByTime(diagnostic.delay); + jest.advanceTimersByTime(delay); expect(setTimeoutMock).toHaveBeenCalledTimes(1); expect(setTimeoutMock.mock.calls[0][0]).toBeInstanceOf(Function); - expect(setTimeoutMock.mock.calls[0][1]).toBe(diagnostic.delay); + expect(setTimeoutMock.mock.calls[0][1]).toBe(delay); setTimeoutMock.mockRestore(); }); }); From d927a2280feb49cbdcf1ffd2233c2f3b034eb7d7 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Fri, 22 Sep 2023 17:07:26 +0800 Subject: [PATCH 17/50] feat: diagnostic options --- packages/analytics-browser/src/config.ts | 8 ++-- .../src/{plugins => }/diagnostic.ts | 0 .../test/{plugins => }/diagnostic.test.ts | 4 +- .../analytics-browser/test/helpers/mock.ts | 3 +- packages/analytics-core/src/config.ts | 14 ++++++- packages/analytics-core/src/diagnostic.ts | 25 +++++------ .../analytics-core/src/plugins/destination.ts | 3 +- packages/analytics-core/test/config.test.ts | 16 +++++++ .../analytics-core/test/diagnostic.test.ts | 33 ++++++++------- .../test/plugins/destination.test.ts | 42 ------------------- packages/analytics-node/test/config.test.ts | 2 + .../test/config.test.ts | 3 ++ packages/analytics-types/src/config/core.ts | 4 +- packages/analytics-types/src/diagnostic.ts | 7 +++- packages/analytics-types/src/index.ts | 2 +- 15 files changed, 83 insertions(+), 83 deletions(-) rename packages/analytics-browser/src/{plugins => }/diagnostic.ts (100%) rename packages/analytics-browser/test/{plugins => }/diagnostic.test.ts (74%) diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index d888f4fb4..e2471eeee 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -14,8 +14,9 @@ import { IngestionMetadata, IdentityStorageType, ServerZoneType, + DiagnosticOptions, } from '@amplitude/analytics-types'; -import { Config, Diagnostic, Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; +import { Config, Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; import { CookieStorage, getCookieName, FetchTransport, getQueryParams } from '@amplitude/analytics-client-common'; import { LocalStorage } from './storage/local-storage'; @@ -26,6 +27,7 @@ import { parseLegacyCookies } from './cookie-migration'; import { CookieOptions } from '@amplitude/analytics-types/lib/esm/config/browser'; import { DEFAULT_IDENTITY_STORAGE, DEFAULT_SERVER_ZONE } from './constants'; import { AmplitudeBrowser } from './browser-client'; +import { Diagnostic } from './diagnostic'; // Exported for testing purposes only. Do not expose to public interface. export class BrowserConfig extends Config implements IBrowserConfig { @@ -76,8 +78,8 @@ export class BrowserConfig extends Config implements IBrowserConfig { }, public transport: 'fetch' | 'xhr' | 'beacon' = 'fetch', public useBatch: boolean = false, + public diagnosticProvider: IDiagnostic | DiagnosticOptions = new Diagnostic(), userId?: string, - public diagnosticProvider: IDiagnostic | undefined = new Diagnostic(), ) { super({ apiKey, storageProvider, transportProvider: createTransport(transport) }); this._cookieStorage = cookieStorage; @@ -249,8 +251,8 @@ export const useBrowserConfig = async ( trackingOptions, options.transport, options.useBatch, - userId, options.diagnosticProvider, + userId, ); }; diff --git a/packages/analytics-browser/src/plugins/diagnostic.ts b/packages/analytics-browser/src/diagnostic.ts similarity index 100% rename from packages/analytics-browser/src/plugins/diagnostic.ts rename to packages/analytics-browser/src/diagnostic.ts diff --git a/packages/analytics-browser/test/plugins/diagnostic.test.ts b/packages/analytics-browser/test/diagnostic.test.ts similarity index 74% rename from packages/analytics-browser/test/plugins/diagnostic.test.ts rename to packages/analytics-browser/test/diagnostic.test.ts index 5aebd896c..ee9d4d5b5 100644 --- a/packages/analytics-browser/test/plugins/diagnostic.test.ts +++ b/packages/analytics-browser/test/diagnostic.test.ts @@ -1,4 +1,4 @@ -import { Diagnostic } from '../../src/plugins/diagnostic'; +import { Diagnostic } from '../src/diagnostic'; describe('Diagnostic', () => { test('should fetch', async () => { @@ -6,7 +6,7 @@ describe('Diagnostic', () => { const fetchMock = jest.fn().mockResolvedValueOnce({} as Response); global.fetch = fetchMock; - await diagnostic.track(5, 200, 'Test message'); + diagnostic.track(5, 200, 'Test message'); await diagnostic.flush(); expect(fetchMock).toHaveBeenCalledTimes(1); diff --git a/packages/analytics-browser/test/helpers/mock.ts b/packages/analytics-browser/test/helpers/mock.ts index ba3dae512..260ccf96a 100644 --- a/packages/analytics-browser/test/helpers/mock.ts +++ b/packages/analytics-browser/test/helpers/mock.ts @@ -1,4 +1,4 @@ -import { Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; +import { Diagnostic, Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; import { BrowserClient, BrowserConfig, LogLevel, UserSession } from '@amplitude/analytics-types'; export const createAmplitudeMock = (): jest.MockedObject => ({ @@ -54,6 +54,7 @@ export const createConfigurationMock = (options?: Partial) => { send: jest.fn(), }, useBatch: false, + diagnosticProvider: new Diagnostic(), // browser config appVersion: undefined, diff --git a/packages/analytics-core/src/config.ts b/packages/analytics-core/src/config.ts index 5e0f681b1..ab0c756a1 100644 --- a/packages/analytics-core/src/config.ts +++ b/packages/analytics-core/src/config.ts @@ -2,6 +2,7 @@ import { Event, Config as IConfig, Diagnostic as IDiagnostic, + DiagnosticOptions, Logger as ILogger, LogLevel, Storage, @@ -19,6 +20,7 @@ import { } from './constants'; import { Logger } from './logger'; +import { Diagnostic } from './diagnostic'; export const getDefaultConfig = () => ({ flushMaxRetries: 12, @@ -31,6 +33,7 @@ export const getDefaultConfig = () => ({ serverUrl: AMPLITUDE_SERVER_URL, serverZone: 'US' as ServerZoneType, useBatch: false, + diagnosticProvider: new Diagnostic(), }); export class Config implements IConfig { @@ -49,7 +52,7 @@ export class Config implements IConfig { transportProvider: Transport; storageProvider?: Storage; useBatch: boolean; - diagnosticProvider?: IDiagnostic; + diagnosticProvider: IDiagnostic | DiagnosticOptions; protected _optOut = false; get optOut() { @@ -77,6 +80,15 @@ export class Config implements IConfig { this.storageProvider = options.storageProvider; this.transportProvider = options.transportProvider; this.useBatch = options.useBatch ?? defaultConfig.useBatch; + + if (options.diagnosticProvider == undefined) { + this.diagnosticProvider = defaultConfig.diagnosticProvider; + } else if (options.diagnosticProvider instanceof Diagnostic) { + this.diagnosticProvider = options.diagnosticProvider; + } else { + this.diagnosticProvider = new Diagnostic(options.diagnosticProvider as DiagnosticOptions); + } + this.loggerProvider.enable(this.logLevel); const serverConfig = createServerConfig(options.serverUrl, options.serverZone, options.useBatch); diff --git a/packages/analytics-core/src/diagnostic.ts b/packages/analytics-core/src/diagnostic.ts index 32099a0a5..073a5c240 100644 --- a/packages/analytics-core/src/diagnostic.ts +++ b/packages/analytics-core/src/diagnostic.ts @@ -1,4 +1,4 @@ -import { Diagnostic as IDiagnostic, Event, Result } from '@amplitude/analytics-types'; +import { Diagnostic as IDiagnostic, DiagnosticOptions } from '@amplitude/analytics-types'; import { DIAGNOSTIC_ENDPOINT } from './constants'; interface DiagnosticEvent { @@ -13,7 +13,8 @@ interface DiagnosticEvent { } export class Diagnostic implements IDiagnostic { - serverUrl: string; + isDisabled = false; + serverUrl: string = DIAGNOSTIC_ENDPOINT; queue: DiagnosticEvent[] = []; private scheduled: ReturnType | null = null; @@ -21,11 +22,16 @@ export class Diagnostic implements IDiagnostic { // make it private to prevent users from changing it to smaller value private delay = 60000; - constructor(serverUrl?: string) { - this.serverUrl = serverUrl ? serverUrl : DIAGNOSTIC_ENDPOINT; + constructor(options?: DiagnosticOptions) { + this.isDisabled = options && options.isDisabled ? options.isDisabled : false; + this.serverUrl = options && options.serverUrl ? options.serverUrl : DIAGNOSTIC_ENDPOINT; } track(eventCount: number, code: number, message: string) { + if (this.isDisabled) { + return; + } + this.queue.push(this.diagnosticEventBuilder(eventCount, code, message)); if (!this.scheduled) { @@ -68,15 +74,4 @@ export class Diagnostic implements IDiagnostic { method: 'POST', }; } - - execute(_context: Event): Promise { - return Promise.resolve({ - event: { event_type: 'diagnostic event' }, - code: -1, - message: 'this method should not be called, use track() instead', - }); - // this method is not implemented - // it's kept here to satisfy the interface - // track() should be used instead - } } diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index a50e9efb3..bea7a2ccf 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -2,6 +2,7 @@ import { Config, DestinationContext as Context, DestinationPlugin, + Diagnostic, Event, InvalidResponse, PayloadTooLargeResponse, @@ -288,7 +289,7 @@ export class Destination implements DestinationPlugin { } fulfillRequest(list: Context[], code: number, message: string) { - this.config.diagnosticProvider?.track(list.length, code, message); + (this.config.diagnosticProvider as Diagnostic).track(list.length, code, message); this.saveEvents(); list.forEach((context) => context.callback(buildResult(context.event, code, message))); } diff --git a/packages/analytics-core/test/config.test.ts b/packages/analytics-core/test/config.test.ts index c449071af..f97462fed 100644 --- a/packages/analytics-core/test/config.test.ts +++ b/packages/analytics-core/test/config.test.ts @@ -8,6 +8,7 @@ import { import { Config, createServerConfig, getServerUrl } from '../src/config'; import { Logger } from '../src/logger'; import { API_KEY, useDefaultConfig } from './helpers/default'; +import { Diagnostic } from '../src/diagnostic'; describe('config', () => { test('should create default config', () => { @@ -35,6 +36,7 @@ describe('config', () => { storageProvider: defaultConfig.storageProvider, transportProvider: defaultConfig.transportProvider, useBatch: false, + diagnosticProvider: defaultConfig.diagnosticProvider, }); expect(config.optOut).toBe(false); }); @@ -53,6 +55,7 @@ describe('config', () => { storageProvider: defaultConfig.storageProvider, transportProvider: defaultConfig.transportProvider, useBatch: true, + diagnosticProvider: { isDisabled: true }, }); expect(config).toEqual({ apiKey: 'apiKey', @@ -76,9 +79,22 @@ describe('config', () => { storageProvider: defaultConfig.storageProvider, transportProvider: defaultConfig.transportProvider, useBatch: true, + diagnosticProvider: new Diagnostic({ isDisabled: true }), }); }); + test('should overwirte diagnostic provider', () => { + const defaultConfig = useDefaultConfig(); + const diagnosticProvider = new Diagnostic({ isDisabled: true }); + const config = new Config({ + apiKey: API_KEY, + storageProvider: defaultConfig.storageProvider, + transportProvider: defaultConfig.transportProvider, + diagnosticProvider: diagnosticProvider, + }); + expect(config.diagnosticProvider).toEqual(diagnosticProvider); + }); + describe('getServerUrl', () => { test('should return eu batch url', () => { expect(getServerUrl('EU', true)).toBe(EU_AMPLITUDE_BATCH_SERVER_URL); diff --git a/packages/analytics-core/test/diagnostic.test.ts b/packages/analytics-core/test/diagnostic.test.ts index 59bbeabaa..2a63e2976 100644 --- a/packages/analytics-core/test/diagnostic.test.ts +++ b/packages/analytics-core/test/diagnostic.test.ts @@ -18,15 +18,23 @@ describe('Diagnostic', () => { }); describe('constructor', () => { - test('should set serverUrl to default value if not provided', () => { + test('should set default values if not provided', () => { expect(diagnostic.serverUrl).toBe(DIAGNOSTIC_ENDPOINT); + expect(diagnostic.isDisabled).toBe(false); + }); + + test('should set isDisabled to provided value', () => { + const isDisabled = true; + diagnostic = new Diagnostic({ isDisabled }); + expect(diagnostic.serverUrl).toBe(DIAGNOSTIC_ENDPOINT); + expect(diagnostic.isDisabled).toBe(isDisabled); }); test('should set serverUrl to provided value', () => { const serverUrl = 'https://test.com'; - diagnostic = new Diagnostic(serverUrl); - + diagnostic = new Diagnostic({ serverUrl }); expect(diagnostic.serverUrl).toBe(serverUrl); + expect(diagnostic.isDisabled).toBe(false); }); }); @@ -41,7 +49,14 @@ describe('Diagnostic', () => { expect(diagnostic.queue[0].library).toBe('diagnostic-test-library'); }); - test('should schedule flush when track is called for the first time 0', () => { + test('should not add to queen when disabled', () => { + diagnostic.isDisabled = true; + diagnostic.track(eventCount, code, 'Test message'); + + expect(diagnostic.queue).toHaveLength(0); + }); + + test('should schedule flush when track is called for the first time', () => { const setTimeoutMock = jest.spyOn(global, 'setTimeout'); diagnostic.track(eventCount, code, 'Test message'); @@ -94,14 +109,4 @@ describe('Diagnostic', () => { expect(diagnostic.requestPayloadBuilder(events)).toEqual(expectedPayload); }); }); - - describe('execute', () => { - test('should not be called, use track() instead', async () => { - const context = { event_type: 'custom event' }; - - const result = await diagnostic.execute(context); - - expect(result.message).toBe('this method should not be called, use track() instead'); - }); - }); }); diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index 2cb7a1cea..6ccab58f0 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -1079,46 +1079,4 @@ describe('destination', () => { expect(result).toBe(''); }); }); - - describe('fulfillRequest', () => { - test('disgnostic should track if it enabled', () => { - const context = { - event: { - event_type: 'event_type', - }, - callback: () => undefined, - attempts: 0, - timeout: 0, - }; - const destination = new Destination(); - destination.config = useDefaultConfig(); - const diagnostic = { - track: () => undefined, - }; - destination.config.diagnosticProvider = diagnostic; - const diagnosticTrack = jest.spyOn(diagnostic, 'track'); - destination.fulfillRequest([context], 0, 'test_message'); - expect(diagnosticTrack).toHaveBeenCalledTimes(1); - }); - - test('disgnostic should not track if it disabled', () => { - const context = { - event: { - event_type: 'event_type', - }, - callback: () => undefined, - attempts: 0, - timeout: 0, - }; - const destination = new Destination(); - destination.config = useDefaultConfig(); - const diagnostic = { - track: () => undefined, - }; - destination.config.diagnosticProvider = undefined; - const diagnosticTrack = jest.spyOn(diagnostic, 'track'); - destination.fulfillRequest([context], 0, 'test_message'); - expect(diagnosticTrack).toHaveBeenCalledTimes(0); - }); - }); }); diff --git a/packages/analytics-node/test/config.test.ts b/packages/analytics-node/test/config.test.ts index ebe0e65f5..6ce5f0e72 100644 --- a/packages/analytics-node/test/config.test.ts +++ b/packages/analytics-node/test/config.test.ts @@ -27,6 +27,7 @@ describe('config', () => { storageProvider: undefined, transportProvider: new Http(), useBatch: false, + diagnosticProvider: new core.Diagnostic(), }); }); }); @@ -56,6 +57,7 @@ describe('config', () => { transportProvider: new Http(), userId: undefined, useBatch: false, + diagnosticProvider: new core.Diagnostic(), }); }); }); diff --git a/packages/analytics-react-native/test/config.test.ts b/packages/analytics-react-native/test/config.test.ts index cc2470b29..6b04fd0a2 100644 --- a/packages/analytics-react-native/test/config.test.ts +++ b/packages/analytics-react-native/test/config.test.ts @@ -60,6 +60,7 @@ describe('config', () => { }, transportProvider: new FetchTransport(), useBatch: false, + diagnosticProvider: new core.Diagnostic(), trackingSessionEvents: false, }); }); @@ -113,6 +114,7 @@ describe('config', () => { }, transportProvider: new FetchTransport(), useBatch: false, + diagnosticProvider: new core.Diagnostic(), trackingSessionEvents: false, }); }); @@ -194,6 +196,7 @@ describe('config', () => { }, transportProvider: new FetchTransport(), useBatch: false, + diagnosticProvider: new core.Diagnostic(), _userId: 'userIdFromCookies', }); }); diff --git a/packages/analytics-types/src/config/core.ts b/packages/analytics-types/src/config/core.ts index c01cbcc07..1a86d730f 100644 --- a/packages/analytics-types/src/config/core.ts +++ b/packages/analytics-types/src/config/core.ts @@ -5,7 +5,7 @@ import { ServerZoneType } from '../server-zone'; import { Storage } from '../storage'; import { Transport } from '../transport'; import { Logger, LogLevel } from '../logger'; -import { Diagnostic } from '../diagnostic'; +import { Diagnostic, DiagnosticOptions } from '../diagnostic'; export interface Config { apiKey: string; @@ -24,7 +24,7 @@ export interface Config { storageProvider?: Storage; transportProvider: Transport; useBatch: boolean; - diagnosticProvider?: Diagnostic; + diagnosticProvider: Diagnostic | DiagnosticOptions; } export interface Options extends Partial { diff --git a/packages/analytics-types/src/diagnostic.ts b/packages/analytics-types/src/diagnostic.ts index 5004319a3..f40e07c3e 100644 --- a/packages/analytics-types/src/diagnostic.ts +++ b/packages/analytics-types/src/diagnostic.ts @@ -1,3 +1,8 @@ -export interface Diagnostic { +export interface DiagnosticOptions { + isDisabled?: boolean; + serverUrl?: string; +} + +export interface Diagnostic extends DiagnosticOptions { track(eventCount: number, code: number, message: string): void; } diff --git a/packages/analytics-types/src/index.ts b/packages/analytics-types/src/index.ts index e3618a0da..7089c1008 100644 --- a/packages/analytics-types/src/index.ts +++ b/packages/analytics-types/src/index.ts @@ -49,7 +49,7 @@ export { Payload } from './payload'; export { Plan } from './plan'; export { IngestionMetadata } from './ingestion-metadata'; export { Plugin, BeforePlugin, EnrichmentPlugin, DestinationPlugin, PluginType } from './plugin'; -export { Diagnostic } from './diagnostic'; +export { Diagnostic, DiagnosticOptions } from './diagnostic'; export { Result } from './result'; export { Response, SuccessResponse, InvalidResponse, PayloadTooLargeResponse, RateLimitResponse } from './response'; export { QueueProxy, InstanceProxy } from './proxy'; From d1911b67beaa4dadb6b840c2ec923a014ab43ec5 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 27 Sep 2023 07:53:08 +1300 Subject: [PATCH 18/50] feat: diagnostic track --- .../analytics-core/src/plugins/destination.ts | 99 ++++++----- .../test/plugins/destination.test.ts | 159 +++++++++++++++++- 2 files changed, 218 insertions(+), 40 deletions(-) diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index bea7a2ccf..87435aa19 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -87,6 +87,7 @@ export class Destination implements DestinationPlugin { return true; } void this.fulfillRequest([context], 500, MAX_RETRIES_EXCEEDED_MESSAGE); + (this.config.diagnosticProvider as Diagnostic).track(list.length, 500, 'exceeded max retries'); return false; }); @@ -155,25 +156,19 @@ export class Destination implements DestinationPlugin { const res = await this.config.transportProvider.send(serverUrl, payload); if (res === null) { this.fulfillRequest(list, 0, UNEXPECTED_ERROR_MESSAGE); + (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, 'unexpected error'); return; } - if (!useRetry) { - if ('body' in res) { - this.fulfillRequest(list, res.statusCode, `${res.status}: ${getResponseBodyString(res)}`); - } else { - this.fulfillRequest(list, res.statusCode, res.status); - } - return; - } - this.handleResponse(res, list); + this.handleResponse(res, list, useRetry); } catch (e) { const errorMessage = getErrorMessage(e); this.config.loggerProvider.error(errorMessage); this.fulfillRequest(list, 0, errorMessage); + (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, 'unexpected error'); } } - handleResponse(res: Response, list: Context[]) { + handleResponse(res: Response, list: Context[], useRetry: boolean) { const { status } = res; switch (status) { @@ -182,22 +177,22 @@ export class Destination implements DestinationPlugin { break; } case Status.Invalid: { - this.handleInvalidResponse(res, list); + this.handleInvalidResponse(res, list, useRetry); break; } case Status.PayloadTooLarge: { - this.handlePayloadTooLargeResponse(res, list); + this.handlePayloadTooLargeResponse(res, list, useRetry); break; } case Status.RateLimit: { - this.handleRateLimitResponse(res, list); + this.handleRateLimitResponse(res, list, useRetry); break; } default: { // log intermediate event status before retry this.config.loggerProvider.warn(`{code: 0, error: "Status '${status}' provided for ${list.length} events"}`); - this.handleOtherResponse(list); + this.handleOtherResponse(res, list, useRetry); break; } } @@ -207,9 +202,10 @@ export class Destination implements DestinationPlugin { this.fulfillRequest(list, res.statusCode, SUCCESS_MESSAGE); } - handleInvalidResponse(res: InvalidResponse, list: Context[]) { + handleInvalidResponse(res: InvalidResponse, list: Context[], useRetry: boolean) { if (res.body.missingField || res.body.error.startsWith(INVALID_API_KEY)) { - this.fulfillRequest(list, res.statusCode, res.body.error); + this.fulfillRequest(list, res.statusCode, `${res.status}: ${getResponseBodyString(res)}`); + (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, 'invalid or missing fields'); return; } @@ -220,24 +216,32 @@ export class Destination implements DestinationPlugin { ...res.body.silencedEvents, ].flat(); const dropIndexSet = new Set(dropIndex); + (this.config.diagnosticProvider as Diagnostic).track( + useRetry ? dropIndexSet.size : list.length, + 400, + 'event error', + ); - const retry = list.filter((context, index) => { - if (dropIndexSet.has(index)) { - this.fulfillRequest([context], res.statusCode, res.body.error); - return; - } - return true; - }); + if (useRetry) { + const retry = list.filter((context, index) => { + if (dropIndexSet.has(index)) { + this.fulfillRequest([context], res.statusCode, res.body.error); + return; + } + return true; + }); - if (retry.length > 0) { - // log intermediate event status before retry - this.config.loggerProvider.warn(getResponseBodyString(res)); + if (retry.length > 0) { + // log intermediate event status before retry + this.config.loggerProvider.warn(getResponseBodyString(res)); + } + this.addToQueue(...retry); } - this.addToQueue(...retry); } - handlePayloadTooLargeResponse(res: PayloadTooLargeResponse, list: Context[]) { - if (list.length === 1) { + handlePayloadTooLargeResponse(res: PayloadTooLargeResponse, list: Context[], useRetry: boolean) { + if (list.length === 1 || !useRetry) { + (this.config.diagnosticProvider as Diagnostic).track(list.length, 413, 'payload too large'); this.fulfillRequest(list, res.statusCode, res.body.error); return; } @@ -249,7 +253,13 @@ export class Destination implements DestinationPlugin { this.addToQueue(...list); } - handleRateLimitResponse(res: RateLimitResponse, list: Context[]) { + handleRateLimitResponse(res: RateLimitResponse, list: Context[], useRetry: boolean) { + if (!useRetry) { + (this.config.diagnosticProvider as Diagnostic).track(list.length, 429, 'exceeded daily quota users or devices'); + this.fulfillRequest(list, res.statusCode, res.status); + return; + } + const dropUserIds = Object.keys(res.body.exceededDailyQuotaUsers); const dropDeviceIds = Object.keys(res.body.exceededDailyQuotaDevices); const throttledIndex = res.body.throttledEvents; @@ -271,6 +281,15 @@ export class Destination implements DestinationPlugin { return true; }); + const dropEvents = list.filter((element) => !retry.includes(element)); + if (dropEvents.length > 0) { + (this.config.diagnosticProvider as Diagnostic).track( + dropEvents.length, + 429, + 'exceeded daily quota users or devices', + ); + } + if (retry.length > 0) { // log intermediate event status before retry this.config.loggerProvider.warn(getResponseBodyString(res)); @@ -279,17 +298,21 @@ export class Destination implements DestinationPlugin { this.addToQueue(...retry); } - handleOtherResponse(list: Context[]) { - this.addToQueue( - ...list.map((context) => { - context.timeout = context.attempts * this.retryTimeout; - return context; - }), - ); + handleOtherResponse(res: Response, list: Context[], useRetry: boolean) { + if (useRetry) { + this.addToQueue( + ...list.map((context) => { + context.timeout = context.attempts * this.retryTimeout; + return context; + }), + ); + } else { + this.fulfillRequest(list, res.statusCode, res.status); + (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, 'unexpected error'); + } } fulfillRequest(list: Context[], code: number, message: string) { - (this.config.diagnosticProvider as Diagnostic).track(list.length, code, message); this.saveEvents(); list.forEach((context) => context.callback(buildResult(context.event, code, message))); } diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index 6ccab58f0..e3c7ba8a0 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -1,5 +1,13 @@ import { Destination, getResponseBodyString } from '../../src/plugins/destination'; -import { Config, DestinationContext, Logger, Payload, Result, Status } from '@amplitude/analytics-types'; +import { + Config, + DestinationContext, + Logger, + Payload, + Result, + Status, + Diagnostic as IDiagnostic, +} from '@amplitude/analytics-types'; import { API_KEY, useDefaultConfig } from '../helpers/default'; import { INVALID_API_KEY, @@ -9,8 +17,18 @@ import { } from '../../src/messages'; const jsons = (obj: any) => JSON.stringify(obj, null, 2); +class Diagnostic implements IDiagnostic { + track = jest.fn(); + isDisabled = false; + serverUrl = 'test'; +} +const diagnosticProvider = new Diagnostic(); describe('destination', () => { + afterEach(() => { + diagnosticProvider.track.mockClear(); + }); + describe('setup', () => { test('should setup plugin', async () => { const destination = new Destination(); @@ -454,7 +472,7 @@ describe('destination', () => { }); }); - test('should handle unexpected error', async () => { + test('should handle unexpected error and track diagnostic', async () => { const destination = new Destination(); const callback = jest.fn(); const context = { @@ -473,9 +491,146 @@ describe('destination', () => { await destination.setup({ ...useDefaultConfig(), transportProvider, + diagnosticProvider, }); await destination.send([context]); expect(callback).toHaveBeenCalledTimes(1); + expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); + expect(diagnosticProvider.track).toHaveBeenLastCalledWith(1, 0, 'unexpected error'); + }); + + test.each([ + ['api_key', undefined, true, 2, 'invalid or missing fields'], + [undefined, { time: [0] }, true, 1, 'event error'], + [undefined, { time: [0] }, false, 2, 'event error'], + ])( + 'should track diagnostic when 400', + async (missingField, eventsWithInvalidFields, useRetry, dropCount, message) => { + const destination = new Destination(); + const callback = jest.fn(); + const transportProvider = { + send: jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ + status: Status.Invalid, + statusCode: 400, + body: { + error: 'error', + missingField: missingField, + eventsWithInvalidFields: eventsWithInvalidFields, + eventsWithMissingFields: {}, + eventsWithInvalidIdLengths: {}, + silencedEvents: [], + }, + }); + }), + }; + await destination.setup({ + ...useDefaultConfig(), + transportProvider, + apiKey: API_KEY, + diagnosticProvider, + }); + await destination.send( + [ + { + attempts: 0, + callback, + event: { + event_type: 'event_type', + user_id: 'user_0', + }, + timeout: 0, + }, + { + attempts: 0, + callback, + event: { + event_type: 'event_type', + user_id: 'user_1', + }, + timeout: 0, + }, + ], + useRetry, + ); + expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); + expect(diagnosticProvider.track).toHaveBeenCalledWith(dropCount, 400, message); + }, + ); + + test('should track diagnostic when 429 and not retry', async () => { + const destination = new Destination(); + const callback = jest.fn(); + const event = { + event_type: 'event_type', + }; + const context = { + attempts: 0, + callback, + event, + timeout: 0, + }; + const transportProvider = { + send: jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ + status: Status.RateLimit, + statusCode: 429, + }); + }), + }; + await destination.setup({ + ...useDefaultConfig(), + transportProvider, + apiKey: API_KEY, + diagnosticProvider, + }); + await destination.send([context], false); + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith({ + event, + code: 429, + message: Status.RateLimit, + }); + expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); + expect(diagnosticProvider.track).toHaveBeenCalledWith(1, 429, 'exceeded daily quota users or devices'); + expect(destination.queue.length).toBe(0); + }); + + test('should track diagnostic when 429 and retry', async () => { + const destination = new Destination(); + const callback = jest.fn(); + const event = { + event_type: 'event_type', + user_id: 'user_0', + }; + const context = { + attempts: 0, + callback, + event, + timeout: 0, + }; + const transportProvider = { + send: jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ + status: Status.RateLimit, + statusCode: 429, + body: { + exceededDailyQuotaUsers: { user_0: 1 }, + exceededDailyQuotaDevices: {}, + throttledEvents: [], + }, + }); + }), + }; + await destination.setup({ + ...useDefaultConfig(), + transportProvider, + apiKey: API_KEY, + diagnosticProvider, + }); + await destination.send([context]); + expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); + expect(diagnosticProvider.track).toHaveBeenCalledWith(1, 429, 'exceeded daily quota users or devices'); }); }); From 4e857cb160a04627ba230688dc9c3dcef7a35e7f Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Thu, 28 Sep 2023 05:59:58 +1300 Subject: [PATCH 19/50] test: fix tests --- .../test/page-view-tracking.test.ts | 3 ++- .../test/web-attribution.test.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts b/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts index 9570fddfd..336da4dfb 100644 --- a/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts +++ b/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts @@ -1,5 +1,5 @@ import { createInstance } from '@amplitude/analytics-browser'; -import { Logger, UUID } from '@amplitude/analytics-core'; +import { Diagnostic, Logger, UUID } from '@amplitude/analytics-core'; import { BrowserConfig, LogLevel } from '@amplitude/analytics-types'; import { pageViewTrackingPlugin, shouldTrackHistoryPageView } from '../src/page-view-tracking'; import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; @@ -31,6 +31,7 @@ describe('pageViewTrackingPlugin', () => { language: true, platform: true, }, + diagnosticProvider: new Diagnostic(), }; beforeAll(() => { diff --git a/packages/plugin-web-attribution-browser/test/web-attribution.test.ts b/packages/plugin-web-attribution-browser/test/web-attribution.test.ts index 6ba4cfc86..ce81a4d9d 100644 --- a/packages/plugin-web-attribution-browser/test/web-attribution.test.ts +++ b/packages/plugin-web-attribution-browser/test/web-attribution.test.ts @@ -3,7 +3,7 @@ import { BASE_CAMPAIGN, CampaignParser, CookieStorage, FetchTransport } from '@a import { webAttributionPlugin } from '../src/web-attribution'; import * as helpers from '../src/helpers'; import { BrowserConfig, LogLevel } from '@amplitude/analytics-types'; -import { Logger, UUID } from '@amplitude/analytics-core'; +import { Diagnostic, Logger, UUID } from '@amplitude/analytics-core'; describe('webAttributionPlugin', () => { const mockConfig: BrowserConfig = { @@ -32,6 +32,7 @@ describe('webAttributionPlugin', () => { language: true, platform: true, }, + diagnosticProvider: new Diagnostic(), }; describe('setup', () => { From f4893a803ca186e78c4b96e31e454aad3c05c6a3 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 27 Sep 2023 07:53:08 +1300 Subject: [PATCH 20/50] feat: diagnostic track --- .../analytics-core/src/plugins/destination.ts | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index 87435aa19..032185d4e 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -135,6 +135,7 @@ export class Destination implements DestinationPlugin { async send(list: Context[], useRetry = true) { if (!this.config.apiKey) { + (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, 'missing API key'); return this.fulfillRequest(list, 400, MISSING_API_KEY_MESSAGE); } @@ -155,6 +156,7 @@ export class Destination implements DestinationPlugin { const { serverUrl } = createServerConfig(this.config.serverUrl, this.config.serverZone, this.config.useBatch); const res = await this.config.transportProvider.send(serverUrl, payload); if (res === null) { + (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, 'unexpected error'); this.fulfillRequest(list, 0, UNEXPECTED_ERROR_MESSAGE); (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, 'unexpected error'); return; @@ -192,7 +194,7 @@ export class Destination implements DestinationPlugin { // log intermediate event status before retry this.config.loggerProvider.warn(`{code: 0, error: "Status '${status}' provided for ${list.length} events"}`); - this.handleOtherResponse(res, list, useRetry); + this.handleOtherResponse(list, useRetry); break; } } @@ -203,9 +205,21 @@ export class Destination implements DestinationPlugin { } handleInvalidResponse(res: InvalidResponse, list: Context[], useRetry: boolean) { - if (res.body.missingField || res.body.error.startsWith(INVALID_API_KEY)) { - this.fulfillRequest(list, res.statusCode, `${res.status}: ${getResponseBodyString(res)}`); - (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, 'invalid or missing fields'); + if (res.body.missingField) { + (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, 'missing fields'); + this.fulfillRequest(list, res.statusCode, res.body.error); + return; + } + + if (res.body.error.startsWith(INVALID_API_KEY)) { + (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, 'invalid API key'); + this.fulfillRequest(list, res.statusCode, res.body.error); + return; + } + + if (!useRetry) { + (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, 'xxx'); + this.fulfillRequest(list, res.statusCode, res.body.error); return; } @@ -216,11 +230,7 @@ export class Destination implements DestinationPlugin { ...res.body.silencedEvents, ].flat(); const dropIndexSet = new Set(dropIndex); - (this.config.diagnosticProvider as Diagnostic).track( - useRetry ? dropIndexSet.size : list.length, - 400, - 'event error', - ); + (this.config.diagnosticProvider as Diagnostic).track(dropIndexSet.size, 400, 'event error'); if (useRetry) { const retry = list.filter((context, index) => { @@ -256,7 +266,7 @@ export class Destination implements DestinationPlugin { handleRateLimitResponse(res: RateLimitResponse, list: Context[], useRetry: boolean) { if (!useRetry) { (this.config.diagnosticProvider as Diagnostic).track(list.length, 429, 'exceeded daily quota users or devices'); - this.fulfillRequest(list, res.statusCode, res.status); + this.fulfillRequest(list, res.statusCode, res.body.error); return; } @@ -281,7 +291,7 @@ export class Destination implements DestinationPlugin { return true; }); - const dropEvents = list.filter((element) => !retry.includes(element)); + const dropEvents = retry.filter((element) => !retry.includes(element)); if (dropEvents.length > 0) { (this.config.diagnosticProvider as Diagnostic).track( dropEvents.length, @@ -298,7 +308,7 @@ export class Destination implements DestinationPlugin { this.addToQueue(...retry); } - handleOtherResponse(res: Response, list: Context[], useRetry: boolean) { + handleOtherResponse(list: Context[], useRetry: boolean) { if (useRetry) { this.addToQueue( ...list.map((context) => { @@ -306,9 +316,6 @@ export class Destination implements DestinationPlugin { return context; }), ); - } else { - this.fulfillRequest(list, res.statusCode, res.status); - (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, 'unexpected error'); } } From 79911202e83490fd2da92926f7beae23bc70ddbb Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Thu, 28 Sep 2023 10:45:26 +1300 Subject: [PATCH 21/50] fix: extract message constants --- packages/analytics-core/src/config.ts | 2 +- .../src/diagnostics/constants.ts | 7 +++ .../src/{ => diagnostics}/diagnostic.ts | 14 +---- .../analytics-core/src/diagnostics/typings.ts | 10 ++++ packages/analytics-core/src/index.ts | 2 +- .../analytics-core/src/plugins/destination.ts | 58 +++++++++++-------- packages/analytics-core/test/config.test.ts | 2 +- .../analytics-core/test/diagnostic.test.ts | 2 +- .../test/plugins/destination.test.ts | 13 +++-- 9 files changed, 65 insertions(+), 45 deletions(-) create mode 100644 packages/analytics-core/src/diagnostics/constants.ts rename packages/analytics-core/src/{ => diagnostics}/diagnostic.ts (87%) create mode 100644 packages/analytics-core/src/diagnostics/typings.ts diff --git a/packages/analytics-core/src/config.ts b/packages/analytics-core/src/config.ts index ab0c756a1..089d206cb 100644 --- a/packages/analytics-core/src/config.ts +++ b/packages/analytics-core/src/config.ts @@ -20,7 +20,7 @@ import { } from './constants'; import { Logger } from './logger'; -import { Diagnostic } from './diagnostic'; +import { Diagnostic } from './diagnostics/diagnostic'; export const getDefaultConfig = () => ({ flushMaxRetries: 12, diff --git a/packages/analytics-core/src/diagnostics/constants.ts b/packages/analytics-core/src/diagnostics/constants.ts new file mode 100644 index 000000000..90930d0ad --- /dev/null +++ b/packages/analytics-core/src/diagnostics/constants.ts @@ -0,0 +1,7 @@ +export const EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE = 'exceeded max retries'; +export const MISSING_API_KEY_DIAGNOSTIC_MESSAGE = 'missing API key'; +export const UNEXPECTED_DIAGNOSTIC_MESSAGE = 'unexpected error'; +export const INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE = 'invalid or missing fields'; +export const EVENT_ERROR_DIAGNOSTIC_MESSAGE = 'event error'; +export const PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE = 'payload too large'; +export const EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE = 'exceeded daily quota users or devices'; diff --git a/packages/analytics-core/src/diagnostic.ts b/packages/analytics-core/src/diagnostics/diagnostic.ts similarity index 87% rename from packages/analytics-core/src/diagnostic.ts rename to packages/analytics-core/src/diagnostics/diagnostic.ts index 073a5c240..069d110f4 100644 --- a/packages/analytics-core/src/diagnostic.ts +++ b/packages/analytics-core/src/diagnostics/diagnostic.ts @@ -1,16 +1,6 @@ import { Diagnostic as IDiagnostic, DiagnosticOptions } from '@amplitude/analytics-types'; -import { DIAGNOSTIC_ENDPOINT } from './constants'; - -interface DiagnosticEvent { - time: number; - event_properties: { - response_error_code: number; - trigger: string; - action: string; - event_count: number; - }; - library: string; -} +import { DIAGNOSTIC_ENDPOINT } from '../constants'; +import { DiagnosticEvent } from './typings'; export class Diagnostic implements IDiagnostic { isDisabled = false; diff --git a/packages/analytics-core/src/diagnostics/typings.ts b/packages/analytics-core/src/diagnostics/typings.ts new file mode 100644 index 000000000..89f1da9a7 --- /dev/null +++ b/packages/analytics-core/src/diagnostics/typings.ts @@ -0,0 +1,10 @@ +export interface DiagnosticEvent { + time: number; + event_properties: { + response_error_code: number; + trigger: string; + action: string; + event_count: number; + }; + library: string; +} diff --git a/packages/analytics-core/src/index.ts b/packages/analytics-core/src/index.ts index a17d893ae..c16abd055 100644 --- a/packages/analytics-core/src/index.ts +++ b/packages/analytics-core/src/index.ts @@ -2,7 +2,7 @@ export { AmplitudeCore } from './core-client'; export { Identify } from './identify'; export { Revenue } from './revenue'; export { Destination } from './plugins/destination'; -export { Diagnostic } from './diagnostic'; +export { Diagnostic } from './diagnostics/diagnostic'; export { Config } from './config'; export { Logger } from './logger'; export { AMPLITUDE_PREFIX, STORAGE_PREFIX } from './constants'; diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index 032185d4e..9cac0bf64 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -23,6 +23,15 @@ import { STORAGE_PREFIX } from '../constants'; import { chunk } from '../utils/chunk'; import { buildResult } from '../utils/result-builder'; import { createServerConfig } from '../config'; +import { + EVENT_ERROR_DIAGNOSTIC_MESSAGE, + EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE, + EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE, + INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE, + MISSING_API_KEY_DIAGNOSTIC_MESSAGE, + PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE, + UNEXPECTED_DIAGNOSTIC_MESSAGE, +} from '../diagnostics/constants'; function getErrorMessage(error: unknown) { if (error instanceof Error) return error.message; @@ -87,7 +96,7 @@ export class Destination implements DestinationPlugin { return true; } void this.fulfillRequest([context], 500, MAX_RETRIES_EXCEEDED_MESSAGE); - (this.config.diagnosticProvider as Diagnostic).track(list.length, 500, 'exceeded max retries'); + (this.config.diagnosticProvider as Diagnostic).track(list.length, 500, EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE); return false; }); @@ -135,7 +144,7 @@ export class Destination implements DestinationPlugin { async send(list: Context[], useRetry = true) { if (!this.config.apiKey) { - (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, 'missing API key'); + (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, MISSING_API_KEY_DIAGNOSTIC_MESSAGE); return this.fulfillRequest(list, 400, MISSING_API_KEY_MESSAGE); } @@ -156,9 +165,9 @@ export class Destination implements DestinationPlugin { const { serverUrl } = createServerConfig(this.config.serverUrl, this.config.serverZone, this.config.useBatch); const res = await this.config.transportProvider.send(serverUrl, payload); if (res === null) { - (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, 'unexpected error'); + (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); this.fulfillRequest(list, 0, UNEXPECTED_ERROR_MESSAGE); - (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, 'unexpected error'); + (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); return; } this.handleResponse(res, list, useRetry); @@ -166,7 +175,7 @@ export class Destination implements DestinationPlugin { const errorMessage = getErrorMessage(e); this.config.loggerProvider.error(errorMessage); this.fulfillRequest(list, 0, errorMessage); - (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, 'unexpected error'); + (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); } } @@ -205,21 +214,13 @@ export class Destination implements DestinationPlugin { } handleInvalidResponse(res: InvalidResponse, list: Context[], useRetry: boolean) { - if (res.body.missingField) { - (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, 'missing fields'); - this.fulfillRequest(list, res.statusCode, res.body.error); - return; - } - - if (res.body.error.startsWith(INVALID_API_KEY)) { - (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, 'invalid API key'); - this.fulfillRequest(list, res.statusCode, res.body.error); - return; - } - - if (!useRetry) { - (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, 'xxx'); - this.fulfillRequest(list, res.statusCode, res.body.error); + if (res.body.missingField || res.body.error.startsWith(INVALID_API_KEY)) { + this.fulfillRequest(list, res.statusCode, `${res.status}: ${getResponseBodyString(res)}`); + (this.config.diagnosticProvider as Diagnostic).track( + list.length, + 400, + INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE, + ); return; } @@ -230,7 +231,11 @@ export class Destination implements DestinationPlugin { ...res.body.silencedEvents, ].flat(); const dropIndexSet = new Set(dropIndex); - (this.config.diagnosticProvider as Diagnostic).track(dropIndexSet.size, 400, 'event error'); + (this.config.diagnosticProvider as Diagnostic).track( + useRetry ? dropIndexSet.size : list.length, + 400, + EVENT_ERROR_DIAGNOSTIC_MESSAGE, + ); if (useRetry) { const retry = list.filter((context, index) => { @@ -251,7 +256,7 @@ export class Destination implements DestinationPlugin { handlePayloadTooLargeResponse(res: PayloadTooLargeResponse, list: Context[], useRetry: boolean) { if (list.length === 1 || !useRetry) { - (this.config.diagnosticProvider as Diagnostic).track(list.length, 413, 'payload too large'); + (this.config.diagnosticProvider as Diagnostic).track(list.length, 413, PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE); this.fulfillRequest(list, res.statusCode, res.body.error); return; } @@ -265,8 +270,8 @@ export class Destination implements DestinationPlugin { handleRateLimitResponse(res: RateLimitResponse, list: Context[], useRetry: boolean) { if (!useRetry) { - (this.config.diagnosticProvider as Diagnostic).track(list.length, 429, 'exceeded daily quota users or devices'); - this.fulfillRequest(list, res.statusCode, res.body.error); + (this.config.diagnosticProvider as Diagnostic).track(list.length, 429, EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE); + this.fulfillRequest(list, res.statusCode, res.status); return; } @@ -296,7 +301,7 @@ export class Destination implements DestinationPlugin { (this.config.diagnosticProvider as Diagnostic).track( dropEvents.length, 429, - 'exceeded daily quota users or devices', + EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE, ); } @@ -316,6 +321,9 @@ export class Destination implements DestinationPlugin { return context; }), ); + } else { + this.fulfillRequest(list, res.statusCode, res.status); + (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); } } diff --git a/packages/analytics-core/test/config.test.ts b/packages/analytics-core/test/config.test.ts index f97462fed..30e6522f1 100644 --- a/packages/analytics-core/test/config.test.ts +++ b/packages/analytics-core/test/config.test.ts @@ -8,7 +8,7 @@ import { import { Config, createServerConfig, getServerUrl } from '../src/config'; import { Logger } from '../src/logger'; import { API_KEY, useDefaultConfig } from './helpers/default'; -import { Diagnostic } from '../src/diagnostic'; +import { Diagnostic } from '../src/diagnostics/diagnostic'; describe('config', () => { test('should create default config', () => { diff --git a/packages/analytics-core/test/diagnostic.test.ts b/packages/analytics-core/test/diagnostic.test.ts index 2a63e2976..9b635a7b3 100644 --- a/packages/analytics-core/test/diagnostic.test.ts +++ b/packages/analytics-core/test/diagnostic.test.ts @@ -1,5 +1,5 @@ import { DIAGNOSTIC_ENDPOINT } from '../src/constants'; -import { Diagnostic } from '../src/diagnostic'; +import { Diagnostic } from '../src/diagnostics/diagnostic'; jest.useFakeTimers(); diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index e3c7ba8a0..fdfb4b480 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -15,6 +15,11 @@ import { SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE, } from '../../src/messages'; +import { + EVENT_ERROR_DIAGNOSTIC_MESSAGE, + INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE, + UNEXPECTED_DIAGNOSTIC_MESSAGE, +} from '../../src/diagnostics/constants'; const jsons = (obj: any) => JSON.stringify(obj, null, 2); class Diagnostic implements IDiagnostic { @@ -496,13 +501,13 @@ describe('destination', () => { await destination.send([context]); expect(callback).toHaveBeenCalledTimes(1); expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); - expect(diagnosticProvider.track).toHaveBeenLastCalledWith(1, 0, 'unexpected error'); + expect(diagnosticProvider.track).toHaveBeenLastCalledWith(1, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); }); test.each([ - ['api_key', undefined, true, 2, 'invalid or missing fields'], - [undefined, { time: [0] }, true, 1, 'event error'], - [undefined, { time: [0] }, false, 2, 'event error'], + ['api_key', undefined, true, 2, INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE], + [undefined, { time: [0] }, true, 1, EVENT_ERROR_DIAGNOSTIC_MESSAGE], + [undefined, { time: [0] }, false, 2, EVENT_ERROR_DIAGNOSTIC_MESSAGE], ])( 'should track diagnostic when 400', async (missingField, eventsWithInvalidFields, useRetry, dropCount, message) => { From b5b8a480ad1e537345a50fee406ad5fb2cbe05fb Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Fri, 29 Sep 2023 11:42:44 +1300 Subject: [PATCH 22/50] test: add more tests for core destination --- .../test/plugins/destination.test.ts | 169 ++++++++++++++---- 1 file changed, 135 insertions(+), 34 deletions(-) diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index fdfb4b480..795bb144b 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -17,7 +17,9 @@ import { } from '../../src/messages'; import { EVENT_ERROR_DIAGNOSTIC_MESSAGE, + EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE, INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE, + PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE, UNEXPECTED_DIAGNOSTIC_MESSAGE, } from '../../src/diagnostics/constants'; @@ -563,23 +565,17 @@ describe('destination', () => { }, ); - test('should track diagnostic when 429 and not retry', async () => { + test('should track diagnostic when 413 and not retry', async () => { const destination = new Destination(); const callback = jest.fn(); - const event = { - event_type: 'event_type', - }; - const context = { - attempts: 0, - callback, - event, - timeout: 0, - }; const transportProvider = { send: jest.fn().mockImplementationOnce(() => { return Promise.resolve({ - status: Status.RateLimit, - statusCode: 429, + status: Status.PayloadTooLarge, + statusCode: 413, + body: { + error: 'error', + }, }); }), }; @@ -589,31 +585,40 @@ describe('destination', () => { apiKey: API_KEY, diagnosticProvider, }); - await destination.send([context], false); - expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith({ - event, - code: 429, - message: Status.RateLimit, - }); + await destination.send( + [ + { + attempts: 0, + callback, + event: { + event_type: 'event_type', + user_id: 'user_0', + }, + timeout: 0, + }, + { + attempts: 0, + callback, + event: { + event_type: 'event_type', + user_id: 'user_1', + }, + timeout: 0, + }, + ], + false, + ); + expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); - expect(diagnosticProvider.track).toHaveBeenCalledWith(1, 429, 'exceeded daily quota users or devices'); - expect(destination.queue.length).toBe(0); + expect(diagnosticProvider.track).toHaveBeenCalledWith(2, 413, PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE); }); - test('should track diagnostic when 429 and retry', async () => { + test.each([ + { useRetry: false, dropCount: 2, queueCount: 0 }, + { useRetry: true, dropCount: 1, queueCount: 1 }, + ])('should track diagnostic when 429', async ({ useRetry, dropCount, queueCount }) => { const destination = new Destination(); const callback = jest.fn(); - const event = { - event_type: 'event_type', - user_id: 'user_0', - }; - const context = { - attempts: 0, - callback, - event, - timeout: 0, - }; const transportProvider = { send: jest.fn().mockImplementationOnce(() => { return Promise.resolve({ @@ -633,10 +638,106 @@ describe('destination', () => { apiKey: API_KEY, diagnosticProvider, }); - await destination.send([context]); + await destination.send( + [ + { + attempts: 0, + callback, + event: { + event_type: 'event_type', + user_id: 'user_0', + }, + timeout: 0, + }, + { + attempts: 0, + callback, + event: { + event_type: 'event_type', + user_id: 'user_1', + }, + timeout: 0, + }, + ], + useRetry, + ); expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); - expect(diagnosticProvider.track).toHaveBeenCalledWith(1, 429, 'exceeded daily quota users or devices'); + expect(diagnosticProvider.track).toHaveBeenCalledWith(dropCount, 429, EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE); + expect(destination.queue.length).toBe(queueCount); }); + // const destination = new Destination(); + // const callback = jest.fn(); + // const event = { + // event_type: 'event_type', + // }; + // const context = { + // attempts: 0, + // callback, + // event, + // timeout: 0, + // }; + // const transportProvider = { + // send: jest.fn().mockImplementationOnce(() => { + // return Promise.resolve({ + // status: Status.RateLimit, + // statusCode: 429, + // }); + // }), + // }; + // await destination.setup({ + // ...useDefaultConfig(), + // transportProvider, + // apiKey: API_KEY, + // diagnosticProvider, + // }); + // await destination.send([context], false); + // expect(callback).toHaveBeenCalledTimes(1); + // expect(callback).toHaveBeenCalledWith({ + // event, + // code: 429, + // message: Status.RateLimit, + // }); + // expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); + // expect(diagnosticProvider.track).toHaveBeenCalledWith(1, 429, 'exceeded daily quota users or devices'); + // expect(destination.queue.length).toBe(0); + // }); + + // test('should track diagnostic when 429 and retry', async () => { + // const destination = new Destination(); + // const callback = jest.fn(); + // const event = { + // event_type: 'event_type', + // user_id: 'user_0', + // }; + // const context = { + // attempts: 0, + // callback, + // event, + // timeout: 0, + // }; + // const transportProvider = { + // send: jest.fn().mockImplementationOnce(() => { + // return Promise.resolve({ + // status: Status.RateLimit, + // statusCode: 429, + // body: { + // exceededDailyQuotaUsers: { user_0: 1 }, + // exceededDailyQuotaDevices: {}, + // throttledEvents: [], + // }, + // }); + // }), + // }; + // await destination.setup({ + // ...useDefaultConfig(), + // transportProvider, + // apiKey: API_KEY, + // diagnosticProvider, + // }); + // await destination.send([context]); + // expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); + // expect(diagnosticProvider.track).toHaveBeenCalledWith(1, 429, 'exceeded daily quota users or devices'); + // }); }); describe('saveEvents', () => { From 56c8af6c857b04be43b576398a0b86aa1a4d9874 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Tue, 10 Oct 2023 21:42:30 -0700 Subject: [PATCH 23/50] refactor: move to diagnostics folder --- packages/analytics-browser/src/config.ts | 2 +- packages/analytics-browser/src/{ => diagnostics}/diagnostic.ts | 0 packages/analytics-browser/test/diagnostic.test.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/analytics-browser/src/{ => diagnostics}/diagnostic.ts (100%) diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index e2471eeee..6ee79ad31 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -27,7 +27,7 @@ import { parseLegacyCookies } from './cookie-migration'; import { CookieOptions } from '@amplitude/analytics-types/lib/esm/config/browser'; import { DEFAULT_IDENTITY_STORAGE, DEFAULT_SERVER_ZONE } from './constants'; import { AmplitudeBrowser } from './browser-client'; -import { Diagnostic } from './diagnostic'; +import { Diagnostic } from './diagnostics/diagnostic'; // Exported for testing purposes only. Do not expose to public interface. export class BrowserConfig extends Config implements IBrowserConfig { diff --git a/packages/analytics-browser/src/diagnostic.ts b/packages/analytics-browser/src/diagnostics/diagnostic.ts similarity index 100% rename from packages/analytics-browser/src/diagnostic.ts rename to packages/analytics-browser/src/diagnostics/diagnostic.ts diff --git a/packages/analytics-browser/test/diagnostic.test.ts b/packages/analytics-browser/test/diagnostic.test.ts index ee9d4d5b5..b69819449 100644 --- a/packages/analytics-browser/test/diagnostic.test.ts +++ b/packages/analytics-browser/test/diagnostic.test.ts @@ -1,4 +1,4 @@ -import { Diagnostic } from '../src/diagnostic'; +import { Diagnostic } from '../src/diagnostics/diagnostic'; describe('Diagnostic', () => { test('should fetch', async () => { From 9492638dbf4f8f1dfa49cac87ce8ec4fbb4e8920 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Tue, 10 Oct 2023 22:41:46 -0700 Subject: [PATCH 24/50] feat: change event structure --- packages/analytics-core/src/config.ts | 1 + .../src/diagnostics/constants.ts | 2 ++ .../src/diagnostics/diagnostic.ts | 13 ++++--- .../analytics-core/src/diagnostics/typings.ts | 20 ++++++----- packages/analytics-core/test/config.test.ts | 2 +- .../analytics-core/test/diagnostic.test.ts | 18 +++++----- .../analytics-core/test/helpers/default.ts | 34 +++++++++++-------- packages/analytics-node/test/config.test.ts | 4 +-- .../test/config.test.ts | 4 +-- packages/analytics-types/src/diagnostic.ts | 1 + 10 files changed, 59 insertions(+), 40 deletions(-) diff --git a/packages/analytics-core/src/config.ts b/packages/analytics-core/src/config.ts index 089d206cb..e6737aa83 100644 --- a/packages/analytics-core/src/config.ts +++ b/packages/analytics-core/src/config.ts @@ -88,6 +88,7 @@ export class Config implements IConfig { } else { this.diagnosticProvider = new Diagnostic(options.diagnosticProvider as DiagnosticOptions); } + this.diagnosticProvider.apiKey = this.apiKey; this.loggerProvider.enable(this.logLevel); diff --git a/packages/analytics-core/src/diagnostics/constants.ts b/packages/analytics-core/src/diagnostics/constants.ts index 90930d0ad..f4c3685a1 100644 --- a/packages/analytics-core/src/diagnostics/constants.ts +++ b/packages/analytics-core/src/diagnostics/constants.ts @@ -5,3 +5,5 @@ export const INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE = 'invalid or missing export const EVENT_ERROR_DIAGNOSTIC_MESSAGE = 'event error'; export const PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE = 'payload too large'; export const EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE = 'exceeded daily quota users or devices'; + +export const DIAGNOSTIC_METADATA_TYPE = 'SDK_DIAGNOSTIC'; diff --git a/packages/analytics-core/src/diagnostics/diagnostic.ts b/packages/analytics-core/src/diagnostics/diagnostic.ts index 069d110f4..97ceae092 100644 --- a/packages/analytics-core/src/diagnostics/diagnostic.ts +++ b/packages/analytics-core/src/diagnostics/diagnostic.ts @@ -1,10 +1,12 @@ import { Diagnostic as IDiagnostic, DiagnosticOptions } from '@amplitude/analytics-types'; import { DIAGNOSTIC_ENDPOINT } from '../constants'; import { DiagnosticEvent } from './typings'; +import { DIAGNOSTIC_METADATA_TYPE } from './constants'; export class Diagnostic implements IDiagnostic { isDisabled = false; serverUrl: string = DIAGNOSTIC_ENDPOINT; + apiKey: string; queue: DiagnosticEvent[] = []; private scheduled: ReturnType | null = null; @@ -15,6 +17,7 @@ export class Diagnostic implements IDiagnostic { constructor(options?: DiagnosticOptions) { this.isDisabled = options && options.isDisabled ? options.isDisabled : false; this.serverUrl = options && options.serverUrl ? options.serverUrl : DIAGNOSTIC_ENDPOINT; + this.apiKey = ''; } track(eventCount: number, code: number, message: string) { @@ -43,14 +46,16 @@ export class Diagnostic implements IDiagnostic { diagnosticEventBuilder(eventCount: number, code: number, message: string): DiagnosticEvent { return { - time: Date.now(), - event_properties: { - response_error_code: code, + api_key: this.apiKey || '', + omni_metrics: { + metadata_type: DIAGNOSTIC_METADATA_TYPE, + library: 'amplitude-ts', + accounting_time_min: Date.now(), + response_code: code, trigger: message, action: 'drop events', event_count: eventCount, }, - library: 'diagnostic-test-library', }; } diff --git a/packages/analytics-core/src/diagnostics/typings.ts b/packages/analytics-core/src/diagnostics/typings.ts index 89f1da9a7..e5fc5fbe2 100644 --- a/packages/analytics-core/src/diagnostics/typings.ts +++ b/packages/analytics-core/src/diagnostics/typings.ts @@ -1,10 +1,14 @@ -export interface DiagnosticEvent { - time: number; - event_properties: { - response_error_code: number; - trigger: string; - action: string; - event_count: number; - }; +export interface DiagnosticOmniMetrics { + metadata_type: string; library: string; + accounting_time_min: number; + response_code: number; + trigger: string; + action: string; + event_count: number; +} + +export interface DiagnosticEvent { + api_key: string; + omni_metrics: DiagnosticOmniMetrics; } diff --git a/packages/analytics-core/test/config.test.ts b/packages/analytics-core/test/config.test.ts index 30e6522f1..2e4d50c26 100644 --- a/packages/analytics-core/test/config.test.ts +++ b/packages/analytics-core/test/config.test.ts @@ -79,7 +79,7 @@ describe('config', () => { storageProvider: defaultConfig.storageProvider, transportProvider: defaultConfig.transportProvider, useBatch: true, - diagnosticProvider: new Diagnostic({ isDisabled: true }), + diagnosticProvider: new Diagnostic({ isDisabled: true, apiKey: API_KEY }), }); }); diff --git a/packages/analytics-core/test/diagnostic.test.ts b/packages/analytics-core/test/diagnostic.test.ts index 9b635a7b3..b71463f5a 100644 --- a/packages/analytics-core/test/diagnostic.test.ts +++ b/packages/analytics-core/test/diagnostic.test.ts @@ -43,10 +43,10 @@ describe('Diagnostic', () => { diagnostic.track(eventCount, code, 'Test message'); expect(diagnostic.queue).toHaveLength(1); - expect(diagnostic.queue[0].event_properties.event_count).toBe(eventCount); - expect(diagnostic.queue[0].event_properties.response_error_code).toBe(code); - expect(diagnostic.queue[0].event_properties.trigger).toBe('Test message'); - expect(diagnostic.queue[0].library).toBe('diagnostic-test-library'); + expect(diagnostic.queue[0].omni_metrics.event_count).toBe(eventCount); + expect(diagnostic.queue[0].omni_metrics.response_code).toBe(code); + expect(diagnostic.queue[0].omni_metrics.trigger).toBe('Test message'); + expect(diagnostic.queue[0].omni_metrics.library).toBe('amplitude-ts'); }); test('should not add to queen when disabled', () => { @@ -86,14 +86,16 @@ describe('Diagnostic', () => { test('should return correct payload', () => { const events = [ { - time: Date.now(), - event_properties: { - response_error_code: code, + api_key: 'test-api-key', + omni_metrics: { + metadata_type: 'diagnostic', + library: 'diagnostic-test-library', + accounting_time_min: Date.now(), + response_code: code, trigger: 'test trigger', action: 'test action', event_count: eventCount, }, - library: 'diagnostic-test-library', }, ]; diff --git a/packages/analytics-core/test/helpers/default.ts b/packages/analytics-core/test/helpers/default.ts index b1fa5d584..2870a2e42 100644 --- a/packages/analytics-core/test/helpers/default.ts +++ b/packages/analytics-core/test/helpers/default.ts @@ -1,21 +1,25 @@ import { Config } from '@amplitude/analytics-types'; import { getDefaultConfig } from '../../src/config'; -export const useDefaultConfig = (): Config => ({ - apiKey: API_KEY, - transportProvider: { - send: () => Promise.resolve(null), - }, - storageProvider: { - isEnabled: async () => true, - get: async () => undefined, - set: async () => undefined, - remove: async () => undefined, - reset: async () => undefined, - getRaw: async () => undefined, - }, - ...getDefaultConfig(), -}); +export const useDefaultConfig = (): Config => { + const defaultConfig = getDefaultConfig(); + defaultConfig.diagnosticProvider.apiKey = API_KEY; + return { + apiKey: API_KEY, + transportProvider: { + send: () => Promise.resolve(null), + }, + storageProvider: { + isEnabled: async () => true, + get: async () => undefined, + set: async () => undefined, + remove: async () => undefined, + reset: async () => undefined, + getRaw: async () => undefined, + }, + ...defaultConfig, + }; +}; export const API_KEY = 'apiKey'; export const USER_ID = 'userId'; diff --git a/packages/analytics-node/test/config.test.ts b/packages/analytics-node/test/config.test.ts index 6ce5f0e72..d3231ab6a 100644 --- a/packages/analytics-node/test/config.test.ts +++ b/packages/analytics-node/test/config.test.ts @@ -27,7 +27,7 @@ describe('config', () => { storageProvider: undefined, transportProvider: new Http(), useBatch: false, - diagnosticProvider: new core.Diagnostic(), + diagnosticProvider: new core.Diagnostic({ apiKey: API_KEY }), }); }); }); @@ -57,7 +57,7 @@ describe('config', () => { transportProvider: new Http(), userId: undefined, useBatch: false, - diagnosticProvider: new core.Diagnostic(), + diagnosticProvider: new core.Diagnostic({ apiKey: API_KEY }), }); }); }); diff --git a/packages/analytics-react-native/test/config.test.ts b/packages/analytics-react-native/test/config.test.ts index 6b04fd0a2..efb053cfd 100644 --- a/packages/analytics-react-native/test/config.test.ts +++ b/packages/analytics-react-native/test/config.test.ts @@ -114,7 +114,7 @@ describe('config', () => { }, transportProvider: new FetchTransport(), useBatch: false, - diagnosticProvider: new core.Diagnostic(), + diagnosticProvider: new core.Diagnostic({ apiKey: API_KEY }), trackingSessionEvents: false, }); }); @@ -196,7 +196,7 @@ describe('config', () => { }, transportProvider: new FetchTransport(), useBatch: false, - diagnosticProvider: new core.Diagnostic(), + diagnosticProvider: new core.Diagnostic({ apiKey: API_KEY }), _userId: 'userIdFromCookies', }); }); diff --git a/packages/analytics-types/src/diagnostic.ts b/packages/analytics-types/src/diagnostic.ts index f40e07c3e..8ad557d67 100644 --- a/packages/analytics-types/src/diagnostic.ts +++ b/packages/analytics-types/src/diagnostic.ts @@ -1,6 +1,7 @@ export interface DiagnosticOptions { isDisabled?: boolean; serverUrl?: string; + apiKey?: string; } export interface Diagnostic extends DiagnosticOptions { From 3df9ac5e38048e0e0ab9e3408baa29237ce44a73 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 11 Oct 2023 00:54:24 -0700 Subject: [PATCH 25/50] test: client level tests --- .../test/browser-client.test.ts | 62 +++++++++++++++- packages/analytics-core/src/index.ts | 9 +++ .../test/plugins/destination.test.ts | 73 ------------------- 3 files changed, 70 insertions(+), 74 deletions(-) diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index 72c858bd3..12f9e3711 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -2,7 +2,7 @@ import { AmplitudeBrowser } from '../src/browser-client'; import * as core from '@amplitude/analytics-core'; import * as Config from '../src/config'; import * as CookieMigration from '../src/cookie-migration'; -import { UserSession } from '@amplitude/analytics-types'; +import { Diagnostic, Status, UserSession } from '@amplitude/analytics-types'; import { CookieStorage, FetchTransport, @@ -261,6 +261,66 @@ describe('browser-client', () => { }).promise; expect(webAttributionPluginPlugin).toHaveBeenCalledTimes(1); }); + + test.each([ + ['api_key', undefined, core.INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE], + [undefined, { time: [0] }, core.EVENT_ERROR_DIAGNOSTIC_MESSAGE], + ])('should diagnostic track when 400 invalid response', async (missingField, eventsWithInvalidFields, message) => { + const transportProvider = { + send: jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ + status: Status.Invalid, + statusCode: 400, + body: { + error: 'error', + missingField: missingField, + eventsWithInvalidFields: eventsWithInvalidFields, + eventsWithMissingFields: {}, + eventsWithInvalidIdLengths: {}, + silencedEvents: [], + }, + }); + }), + }; + + await client.init(apiKey, { + defaultTracking: false, + }).promise; + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + client.config.transportProvider = transportProvider; + await client.track('event_type', { userId: 'user_0' }).promise; + + expect(diagnosticTrack).toHaveBeenCalledTimes(1); + expect(diagnosticTrack).toHaveBeenCalledWith(1, 400, message); + }); + + test('should diagnostic track when 429 rate limit when flush', async () => { + const transportProvider = { + send: jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ + status: Status.RateLimit, + statusCode: 429, + body: { + exceededDailyQuotaUsers: { user_0: 1 }, + exceededDailyQuotaDevices: {}, + throttledEvents: [], + }, + }); + }), + }; + + await client.init(apiKey, { + defaultTracking: false, + }).promise; + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + client.config.transportProvider = transportProvider; + client.track('event_type', { userId: 'user_0' }); + // flush() calls destination.flush(useRetry: false) + await client.flush().promise; + + expect(diagnosticTrack).toHaveBeenCalledTimes(1); + expect(diagnosticTrack).toHaveBeenCalledWith(1, 429, core.EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE); + }); }); describe('getUserId', () => { diff --git a/packages/analytics-core/src/index.ts b/packages/analytics-core/src/index.ts index c16abd055..052283a19 100644 --- a/packages/analytics-core/src/index.ts +++ b/packages/analytics-core/src/index.ts @@ -3,6 +3,15 @@ export { Identify } from './identify'; export { Revenue } from './revenue'; export { Destination } from './plugins/destination'; export { Diagnostic } from './diagnostics/diagnostic'; +export { + EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE, + MISSING_API_KEY_DIAGNOSTIC_MESSAGE, + UNEXPECTED_DIAGNOSTIC_MESSAGE, + INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE, + EVENT_ERROR_DIAGNOSTIC_MESSAGE, + PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE, + EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE, +} from './diagnostics/constants'; export { Config } from './config'; export { Logger } from './logger'; export { AMPLITUDE_PREFIX, STORAGE_PREFIX } from './constants'; diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index 795bb144b..f382a4ed0 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -665,79 +665,6 @@ describe('destination', () => { expect(diagnosticProvider.track).toHaveBeenCalledWith(dropCount, 429, EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE); expect(destination.queue.length).toBe(queueCount); }); - // const destination = new Destination(); - // const callback = jest.fn(); - // const event = { - // event_type: 'event_type', - // }; - // const context = { - // attempts: 0, - // callback, - // event, - // timeout: 0, - // }; - // const transportProvider = { - // send: jest.fn().mockImplementationOnce(() => { - // return Promise.resolve({ - // status: Status.RateLimit, - // statusCode: 429, - // }); - // }), - // }; - // await destination.setup({ - // ...useDefaultConfig(), - // transportProvider, - // apiKey: API_KEY, - // diagnosticProvider, - // }); - // await destination.send([context], false); - // expect(callback).toHaveBeenCalledTimes(1); - // expect(callback).toHaveBeenCalledWith({ - // event, - // code: 429, - // message: Status.RateLimit, - // }); - // expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); - // expect(diagnosticProvider.track).toHaveBeenCalledWith(1, 429, 'exceeded daily quota users or devices'); - // expect(destination.queue.length).toBe(0); - // }); - - // test('should track diagnostic when 429 and retry', async () => { - // const destination = new Destination(); - // const callback = jest.fn(); - // const event = { - // event_type: 'event_type', - // user_id: 'user_0', - // }; - // const context = { - // attempts: 0, - // callback, - // event, - // timeout: 0, - // }; - // const transportProvider = { - // send: jest.fn().mockImplementationOnce(() => { - // return Promise.resolve({ - // status: Status.RateLimit, - // statusCode: 429, - // body: { - // exceededDailyQuotaUsers: { user_0: 1 }, - // exceededDailyQuotaDevices: {}, - // throttledEvents: [], - // }, - // }); - // }), - // }; - // await destination.setup({ - // ...useDefaultConfig(), - // transportProvider, - // apiKey: API_KEY, - // diagnosticProvider, - // }); - // await destination.send([context]); - // expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); - // expect(diagnosticProvider.track).toHaveBeenCalledWith(1, 429, 'exceeded daily quota users or devices'); - // }); }); describe('saveEvents', () => { From 2b4c1f7d2c266ab929d1a64ffb050ab823882d4b Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 11 Oct 2023 09:18:06 -0700 Subject: [PATCH 26/50] test: apikey --- .../analytics-core/src/diagnostics/diagnostic.ts | 6 ++++-- packages/analytics-core/test/index.test.ts | 14 ++++++++++++++ .../analytics-react-native/test/config.test.ts | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/analytics-core/src/diagnostics/diagnostic.ts b/packages/analytics-core/src/diagnostics/diagnostic.ts index 97ceae092..acc488ec2 100644 --- a/packages/analytics-core/src/diagnostics/diagnostic.ts +++ b/packages/analytics-core/src/diagnostics/diagnostic.ts @@ -6,7 +6,7 @@ import { DIAGNOSTIC_METADATA_TYPE } from './constants'; export class Diagnostic implements IDiagnostic { isDisabled = false; serverUrl: string = DIAGNOSTIC_ENDPOINT; - apiKey: string; + apiKey?: string = ''; queue: DiagnosticEvent[] = []; private scheduled: ReturnType | null = null; @@ -17,7 +17,9 @@ export class Diagnostic implements IDiagnostic { constructor(options?: DiagnosticOptions) { this.isDisabled = options && options.isDisabled ? options.isDisabled : false; this.serverUrl = options && options.serverUrl ? options.serverUrl : DIAGNOSTIC_ENDPOINT; - this.apiKey = ''; + if (options && options.apiKey) { + this.apiKey = options.apiKey; + } } track(eventCount: number, code: number, message: string) { diff --git a/packages/analytics-core/test/index.test.ts b/packages/analytics-core/test/index.test.ts index 514a6d74e..4cfceafa7 100644 --- a/packages/analytics-core/test/index.test.ts +++ b/packages/analytics-core/test/index.test.ts @@ -16,6 +16,13 @@ import { createIdentifyEvent, Diagnostic, buildResult, + EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE, + MISSING_API_KEY_DIAGNOSTIC_MESSAGE, + UNEXPECTED_DIAGNOSTIC_MESSAGE, + INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE, + EVENT_ERROR_DIAGNOSTIC_MESSAGE, + PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE, + EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE, } from '../src/index'; describe('index', () => { @@ -46,5 +53,12 @@ describe('index', () => { expect(typeof buildResult).toBe('function'); expect(AMPLITUDE_PREFIX).toBe('AMP'); expect(STORAGE_PREFIX).toBe('AMP_unsent'); + expect(EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE).toBe('exceeded max retries'); + expect(MISSING_API_KEY_DIAGNOSTIC_MESSAGE).toBe('missing API key'); + expect(UNEXPECTED_DIAGNOSTIC_MESSAGE).toBe('unexpected error'); + expect(INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE).toBe('invalid or missing fields'); + expect(EVENT_ERROR_DIAGNOSTIC_MESSAGE).toBe('event error'); + expect(PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE).toBe('payload too large'); + expect(EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE).toBe('exceeded daily quota users or devices'); }); }); diff --git a/packages/analytics-react-native/test/config.test.ts b/packages/analytics-react-native/test/config.test.ts index efb053cfd..20e6cecf5 100644 --- a/packages/analytics-react-native/test/config.test.ts +++ b/packages/analytics-react-native/test/config.test.ts @@ -60,7 +60,7 @@ describe('config', () => { }, transportProvider: new FetchTransport(), useBatch: false, - diagnosticProvider: new core.Diagnostic(), + diagnosticProvider: new core.Diagnostic({ apiKey: '' }), trackingSessionEvents: false, }); }); From bef4da7f9dd3a188ab99229f3d24ca38064dedd4 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 11 Oct 2023 09:29:25 -0700 Subject: [PATCH 27/50] fix: delete duplicate diagnostic track --- packages/analytics-core/src/plugins/destination.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index 9cac0bf64..6ccc551ce 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -167,7 +167,6 @@ export class Destination implements DestinationPlugin { if (res === null) { (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); this.fulfillRequest(list, 0, UNEXPECTED_ERROR_MESSAGE); - (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); return; } this.handleResponse(res, list, useRetry); From da7baf24907df975a5f7b34ae39420f8f7100072 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 11 Oct 2023 11:08:18 -0700 Subject: [PATCH 28/50] test: more client level tests --- .../test/browser-client.test.ts | 207 +++++++++++++----- .../analytics-core/src/plugins/destination.ts | 10 +- 2 files changed, 157 insertions(+), 60 deletions(-) diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index 12f9e3711..1f9b8c16e 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -262,64 +262,161 @@ describe('browser-client', () => { expect(webAttributionPluginPlugin).toHaveBeenCalledTimes(1); }); - test.each([ - ['api_key', undefined, core.INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE], - [undefined, { time: [0] }, core.EVENT_ERROR_DIAGNOSTIC_MESSAGE], - ])('should diagnostic track when 400 invalid response', async (missingField, eventsWithInvalidFields, message) => { - const transportProvider = { - send: jest.fn().mockImplementationOnce(() => { - return Promise.resolve({ - status: Status.Invalid, - statusCode: 400, - body: { - error: 'error', - missingField: missingField, - eventsWithInvalidFields: eventsWithInvalidFields, - eventsWithMissingFields: {}, - eventsWithInvalidIdLengths: {}, - silencedEvents: [], - }, - }); - }), - }; + describe('diagnostic', () => { + test('should not diagnostic track when 200', async () => { + const transportProvider = { + send: jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ + status: Status.Success, + statusCode: 200, + }); + }), + }; + + await client.init(apiKey, { + defaultTracking: false, + }).promise; + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + client.config.transportProvider = transportProvider; + await client.track('event_type', { userId: 'user_0' }).promise; + + expect(diagnosticTrack).toHaveBeenCalledTimes(0); + }); - await client.init(apiKey, { - defaultTracking: false, - }).promise; - const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); - client.config.transportProvider = transportProvider; - await client.track('event_type', { userId: 'user_0' }).promise; - - expect(diagnosticTrack).toHaveBeenCalledTimes(1); - expect(diagnosticTrack).toHaveBeenCalledWith(1, 400, message); - }); - - test('should diagnostic track when 429 rate limit when flush', async () => { - const transportProvider = { - send: jest.fn().mockImplementationOnce(() => { - return Promise.resolve({ - status: Status.RateLimit, - statusCode: 429, - body: { - exceededDailyQuotaUsers: { user_0: 1 }, - exceededDailyQuotaDevices: {}, - throttledEvents: [], - }, - }); - }), - }; + test.each([null, new Error()])('should diagnostic track when 0 unexpected error', async (res) => { + const transportProvider = { + send: jest.fn().mockImplementationOnce(() => { + return Promise.resolve(res); + }), + }; + + await client.init(apiKey, { + defaultTracking: false, + }).promise; + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + client.config.transportProvider = transportProvider; + await client.track('event_type', { userId: 'user_0' }).promise; + + expect(diagnosticTrack).toHaveBeenCalledTimes(1); + expect(diagnosticTrack).toHaveBeenCalledWith(1, 0, core.UNEXPECTED_DIAGNOSTIC_MESSAGE); + }); - await client.init(apiKey, { - defaultTracking: false, - }).promise; - const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); - client.config.transportProvider = transportProvider; - client.track('event_type', { userId: 'user_0' }); - // flush() calls destination.flush(useRetry: false) - await client.flush().promise; + test.each([ + ['api_key', undefined, core.INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE], + [undefined, { time: [0] }, core.EVENT_ERROR_DIAGNOSTIC_MESSAGE], + ])( + 'should diagnostic track when 400 invalid response', + async (missingField, eventsWithInvalidFields, message) => { + const transportProvider = { + send: jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ + status: Status.Invalid, + statusCode: 400, + body: { + error: 'error', + missingField: missingField, + eventsWithInvalidFields: eventsWithInvalidFields, + eventsWithMissingFields: {}, + eventsWithInvalidIdLengths: {}, + silencedEvents: [], + }, + }); + }), + }; + + await client.init(apiKey, { + defaultTracking: false, + }).promise; + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + client.config.transportProvider = transportProvider; + await client.track('event_type', { userId: 'user_0' }).promise; + + expect(diagnosticTrack).toHaveBeenCalledTimes(1); + expect(diagnosticTrack).toHaveBeenCalledWith(1, 400, message); + }, + ); - expect(diagnosticTrack).toHaveBeenCalledTimes(1); - expect(diagnosticTrack).toHaveBeenCalledWith(1, 429, core.EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE); + test('should diagnostic track when 429 rate limit when flush', async () => { + const transportProvider = { + send: jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ + status: Status.RateLimit, + statusCode: 429, + body: { + exceededDailyQuotaUsers: { user_0: 1 }, + exceededDailyQuotaDevices: {}, + throttledEvents: [], + }, + }); + }), + }; + + await client.init(apiKey, { + defaultTracking: false, + }).promise; + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + client.config.transportProvider = transportProvider; + client.track('event_type', { userId: 'user_0' }); + // flush() calls destination.flush(useRetry: false) + await client.flush().promise; + + expect(diagnosticTrack).toHaveBeenCalledTimes(1); + expect(diagnosticTrack).toHaveBeenCalledWith(1, 429, core.EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE); + }); + + test('should diagnostic track when 413', async () => { + const transportProvider = { + send: jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ + status: Status.PayloadTooLarge, + statusCode: 413, + body: { + error: 'error', + }, + }); + }), + }; + + await client.init(apiKey, { + defaultTracking: false, + }).promise; + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + client.config.transportProvider = transportProvider; + await client.track('event_type', { userId: 'user_0' }).promise; + + expect(diagnosticTrack).toHaveBeenCalledTimes(1); + expect(diagnosticTrack).toHaveBeenCalledWith(1, 413, core.PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE); + }); + + test('should diagnostic track when 500 hit max retries', async () => { + const transportProvider = { + send: jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ + status: Status.Invalid, + statusCode: 400, + body: { + error: 'error', + missingField: '', + eventsWithInvalidFields: {}, + eventsWithMissingFields: {}, + eventsWithInvalidIdLengths: {}, + silencedEvents: [], + }, + }); + }), + }; + + await client.init(apiKey, { + defaultTracking: false, + flushMaxRetries: 1, + }).promise; + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + client.config.transportProvider = transportProvider; + await client.track('event_type', { userId: 'user_0' }).promise; + + expect(diagnosticTrack).toHaveBeenCalledTimes(1); + expect(diagnosticTrack).toHaveBeenCalledWith(1, 500, core.EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE); + }); }); }); diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index 6ccc551ce..8b584bf64 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -230,13 +230,11 @@ export class Destination implements DestinationPlugin { ...res.body.silencedEvents, ].flat(); const dropIndexSet = new Set(dropIndex); - (this.config.diagnosticProvider as Diagnostic).track( - useRetry ? dropIndexSet.size : list.length, - 400, - EVENT_ERROR_DIAGNOSTIC_MESSAGE, - ); if (useRetry) { + if (dropIndexSet.size) { + (this.config.diagnosticProvider as Diagnostic).track(dropIndexSet.size, 400, EVENT_ERROR_DIAGNOSTIC_MESSAGE); + } const retry = list.filter((context, index) => { if (dropIndexSet.has(index)) { this.fulfillRequest([context], res.statusCode, res.body.error); @@ -250,6 +248,8 @@ export class Destination implements DestinationPlugin { this.config.loggerProvider.warn(getResponseBodyString(res)); } this.addToQueue(...retry); + } else { + (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, EVENT_ERROR_DIAGNOSTIC_MESSAGE); } } From 6975d5b931b11aa5cb08f013fe3ef0bb2b64f8e8 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 11 Oct 2023 11:10:50 -0700 Subject: [PATCH 29/50] fix: empty commit From 7d7a33c024cba74e5a2728f2817352a715639458 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 11 Oct 2023 11:23:43 -0700 Subject: [PATCH 30/50] fix: update endpoint url --- packages/analytics-core/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/analytics-core/src/constants.ts b/packages/analytics-core/src/constants.ts index e8f66d849..c3bbe57b0 100644 --- a/packages/analytics-core/src/constants.ts +++ b/packages/analytics-core/src/constants.ts @@ -5,4 +5,4 @@ export const AMPLITUDE_SERVER_URL = 'https://api2.amplitude.com/2/httpapi'; export const EU_AMPLITUDE_SERVER_URL = 'https://api.eu.amplitude.com/2/httpapi'; export const AMPLITUDE_BATCH_SERVER_URL = 'https://api2.amplitude.com/batch'; export const EU_AMPLITUDE_BATCH_SERVER_URL = 'https://api.eu.amplitude.com/batch'; -export const DIAGNOSTIC_ENDPOINT = 'http://localhost:8000'; +export const DIAGNOSTIC_ENDPOINT = 'https://api-omni.stag2.amplitude.com/omni/metrics'; From 4723f6b6e26d33b447a73ed818263ae8a2419d1d Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 11 Oct 2023 14:40:57 -0700 Subject: [PATCH 31/50] refactor: rename --- packages/analytics-browser/src/config.ts | 6 +++--- .../src/diagnostics/diagnostic.ts | 4 ++-- packages/analytics-browser/test/config.test.ts | 2 +- packages/analytics-browser/test/diagnostic.test.ts | 4 ++-- packages/analytics-browser/test/helpers/mock.ts | 5 +++-- packages/analytics-core/src/config.ts | 12 ++++++------ .../analytics-core/src/diagnostics/diagnostic.ts | 4 ++-- packages/analytics-core/src/index.ts | 2 +- packages/analytics-core/test/config.test.ts | 6 +++--- packages/analytics-core/test/diagnostic.test.ts | 10 +++++----- packages/analytics-core/test/index.test.ts | 4 ++-- .../test/plugins/destination.test.ts | 14 +++----------- packages/analytics-node/test/config.test.ts | 4 ++-- .../analytics-react-native/test/config.test.ts | 6 +++--- .../test/page-view-tracking.test.ts | 4 ++-- .../test/web-attribution.test.ts | 4 ++-- 16 files changed, 42 insertions(+), 49 deletions(-) diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index 6ee79ad31..b6281bda6 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -3,7 +3,7 @@ import { BrowserOptions, BrowserConfig as IBrowserConfig, DefaultTrackingOptions, - Diagnostic as IDiagnostic, + Diagnostic, Storage, TrackingOptions, TransportType, @@ -27,7 +27,7 @@ import { parseLegacyCookies } from './cookie-migration'; import { CookieOptions } from '@amplitude/analytics-types/lib/esm/config/browser'; import { DEFAULT_IDENTITY_STORAGE, DEFAULT_SERVER_ZONE } from './constants'; import { AmplitudeBrowser } from './browser-client'; -import { Diagnostic } from './diagnostics/diagnostic'; +import { BrowserDiagnostic } from './diagnostics/diagnostic'; // Exported for testing purposes only. Do not expose to public interface. export class BrowserConfig extends Config implements IBrowserConfig { @@ -78,7 +78,7 @@ export class BrowserConfig extends Config implements IBrowserConfig { }, public transport: 'fetch' | 'xhr' | 'beacon' = 'fetch', public useBatch: boolean = false, - public diagnosticProvider: IDiagnostic | DiagnosticOptions = new Diagnostic(), + public diagnosticProvider: Diagnostic | DiagnosticOptions = new BrowserDiagnostic(), userId?: string, ) { super({ apiKey, storageProvider, transportProvider: createTransport(transport) }); diff --git a/packages/analytics-browser/src/diagnostics/diagnostic.ts b/packages/analytics-browser/src/diagnostics/diagnostic.ts index 315799c93..223044313 100644 --- a/packages/analytics-browser/src/diagnostics/diagnostic.ts +++ b/packages/analytics-browser/src/diagnostics/diagnostic.ts @@ -1,6 +1,6 @@ -import { Diagnostic as CoreDiagnostic } from '@amplitude/analytics-core'; +import { BaseDiagnostic } from '@amplitude/analytics-core'; -export class Diagnostic extends CoreDiagnostic { +export class BrowserDiagnostic extends BaseDiagnostic { async flush(): Promise { await fetch(this.serverUrl, this.requestPayloadBuilder(this.queue)); await super.flush(); diff --git a/packages/analytics-browser/test/config.test.ts b/packages/analytics-browser/test/config.test.ts index c32caf2b5..30f20ac81 100644 --- a/packages/analytics-browser/test/config.test.ts +++ b/packages/analytics-browser/test/config.test.ts @@ -20,7 +20,7 @@ describe('config', () => { const someLocalStorage: LocalStorageModule.LocalStorage = expect.any( LocalStorageModule.LocalStorage, ) as LocalStorageModule.LocalStorage; - const someDiagnosticProvider: core.Diagnostic = expect.any(core.Diagnostic) as core.Diagnostic; + const someDiagnosticProvider: core.BaseDiagnostic = expect.any(core.BaseDiagnostic) as core.BaseDiagnostic; let apiKey = ''; diff --git a/packages/analytics-browser/test/diagnostic.test.ts b/packages/analytics-browser/test/diagnostic.test.ts index b69819449..e51c6ebac 100644 --- a/packages/analytics-browser/test/diagnostic.test.ts +++ b/packages/analytics-browser/test/diagnostic.test.ts @@ -1,8 +1,8 @@ -import { Diagnostic } from '../src/diagnostics/diagnostic'; +import { BrowserDiagnostic } from '../src/diagnostics/diagnostic'; describe('Diagnostic', () => { test('should fetch', async () => { - const diagnostic = new Diagnostic(); + const diagnostic = new BrowserDiagnostic(); const fetchMock = jest.fn().mockResolvedValueOnce({} as Response); global.fetch = fetchMock; diff --git a/packages/analytics-browser/test/helpers/mock.ts b/packages/analytics-browser/test/helpers/mock.ts index 260ccf96a..073ed8d68 100644 --- a/packages/analytics-browser/test/helpers/mock.ts +++ b/packages/analytics-browser/test/helpers/mock.ts @@ -1,5 +1,6 @@ -import { Diagnostic, Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; +import { Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; import { BrowserClient, BrowserConfig, LogLevel, UserSession } from '@amplitude/analytics-types'; +import { BrowserDiagnostic } from '../../src/diagnostics/diagnostic'; export const createAmplitudeMock = (): jest.MockedObject => ({ init: jest.fn(), @@ -54,7 +55,6 @@ export const createConfigurationMock = (options?: Partial) => { send: jest.fn(), }, useBatch: false, - diagnosticProvider: new Diagnostic(), // browser config appVersion: undefined, @@ -79,6 +79,7 @@ export const createConfigurationMock = (options?: Partial) => { }, trackingSessionEvents: false, userId: undefined, + diagnosticProvider: new BrowserDiagnostic(), ...options, }; }; diff --git a/packages/analytics-core/src/config.ts b/packages/analytics-core/src/config.ts index e6737aa83..565f64556 100644 --- a/packages/analytics-core/src/config.ts +++ b/packages/analytics-core/src/config.ts @@ -1,7 +1,7 @@ import { Event, Config as IConfig, - Diagnostic as IDiagnostic, + Diagnostic, DiagnosticOptions, Logger as ILogger, LogLevel, @@ -20,7 +20,7 @@ import { } from './constants'; import { Logger } from './logger'; -import { Diagnostic } from './diagnostics/diagnostic'; +import { BaseDiagnostic } from './diagnostics/diagnostic'; export const getDefaultConfig = () => ({ flushMaxRetries: 12, @@ -33,7 +33,7 @@ export const getDefaultConfig = () => ({ serverUrl: AMPLITUDE_SERVER_URL, serverZone: 'US' as ServerZoneType, useBatch: false, - diagnosticProvider: new Diagnostic(), + diagnosticProvider: new BaseDiagnostic(), }); export class Config implements IConfig { @@ -52,7 +52,7 @@ export class Config implements IConfig { transportProvider: Transport; storageProvider?: Storage; useBatch: boolean; - diagnosticProvider: IDiagnostic | DiagnosticOptions; + diagnosticProvider: Diagnostic | DiagnosticOptions; protected _optOut = false; get optOut() { @@ -83,10 +83,10 @@ export class Config implements IConfig { if (options.diagnosticProvider == undefined) { this.diagnosticProvider = defaultConfig.diagnosticProvider; - } else if (options.diagnosticProvider instanceof Diagnostic) { + } else if (options.diagnosticProvider instanceof BaseDiagnostic) { this.diagnosticProvider = options.diagnosticProvider; } else { - this.diagnosticProvider = new Diagnostic(options.diagnosticProvider as DiagnosticOptions); + this.diagnosticProvider = new BaseDiagnostic(options.diagnosticProvider as DiagnosticOptions); } this.diagnosticProvider.apiKey = this.apiKey; diff --git a/packages/analytics-core/src/diagnostics/diagnostic.ts b/packages/analytics-core/src/diagnostics/diagnostic.ts index acc488ec2..76a5c1a2d 100644 --- a/packages/analytics-core/src/diagnostics/diagnostic.ts +++ b/packages/analytics-core/src/diagnostics/diagnostic.ts @@ -1,9 +1,9 @@ -import { Diagnostic as IDiagnostic, DiagnosticOptions } from '@amplitude/analytics-types'; +import { Diagnostic, DiagnosticOptions } from '@amplitude/analytics-types'; import { DIAGNOSTIC_ENDPOINT } from '../constants'; import { DiagnosticEvent } from './typings'; import { DIAGNOSTIC_METADATA_TYPE } from './constants'; -export class Diagnostic implements IDiagnostic { +export class BaseDiagnostic implements Diagnostic { isDisabled = false; serverUrl: string = DIAGNOSTIC_ENDPOINT; apiKey?: string = ''; diff --git a/packages/analytics-core/src/index.ts b/packages/analytics-core/src/index.ts index 052283a19..ce714d81c 100644 --- a/packages/analytics-core/src/index.ts +++ b/packages/analytics-core/src/index.ts @@ -2,7 +2,7 @@ export { AmplitudeCore } from './core-client'; export { Identify } from './identify'; export { Revenue } from './revenue'; export { Destination } from './plugins/destination'; -export { Diagnostic } from './diagnostics/diagnostic'; +export { BaseDiagnostic } from './diagnostics/diagnostic'; export { EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE, MISSING_API_KEY_DIAGNOSTIC_MESSAGE, diff --git a/packages/analytics-core/test/config.test.ts b/packages/analytics-core/test/config.test.ts index 2e4d50c26..9f00a4acc 100644 --- a/packages/analytics-core/test/config.test.ts +++ b/packages/analytics-core/test/config.test.ts @@ -8,7 +8,7 @@ import { import { Config, createServerConfig, getServerUrl } from '../src/config'; import { Logger } from '../src/logger'; import { API_KEY, useDefaultConfig } from './helpers/default'; -import { Diagnostic } from '../src/diagnostics/diagnostic'; +import { BaseDiagnostic } from '../src/diagnostics/diagnostic'; describe('config', () => { test('should create default config', () => { @@ -79,13 +79,13 @@ describe('config', () => { storageProvider: defaultConfig.storageProvider, transportProvider: defaultConfig.transportProvider, useBatch: true, - diagnosticProvider: new Diagnostic({ isDisabled: true, apiKey: API_KEY }), + diagnosticProvider: new BaseDiagnostic({ isDisabled: true, apiKey: API_KEY }), }); }); test('should overwirte diagnostic provider', () => { const defaultConfig = useDefaultConfig(); - const diagnosticProvider = new Diagnostic({ isDisabled: true }); + const diagnosticProvider = new BaseDiagnostic({ isDisabled: true }); const config = new Config({ apiKey: API_KEY, storageProvider: defaultConfig.storageProvider, diff --git a/packages/analytics-core/test/diagnostic.test.ts b/packages/analytics-core/test/diagnostic.test.ts index b71463f5a..517eda347 100644 --- a/packages/analytics-core/test/diagnostic.test.ts +++ b/packages/analytics-core/test/diagnostic.test.ts @@ -1,16 +1,16 @@ import { DIAGNOSTIC_ENDPOINT } from '../src/constants'; -import { Diagnostic } from '../src/diagnostics/diagnostic'; +import { BaseDiagnostic } from '../src/diagnostics/diagnostic'; jest.useFakeTimers(); describe('Diagnostic', () => { - let diagnostic: Diagnostic; + let diagnostic: BaseDiagnostic; const eventCount = 5; const code = 200; const delay = 60000; beforeEach(() => { - diagnostic = new Diagnostic(); + diagnostic = new BaseDiagnostic(); }); afterEach(() => { @@ -25,14 +25,14 @@ describe('Diagnostic', () => { test('should set isDisabled to provided value', () => { const isDisabled = true; - diagnostic = new Diagnostic({ isDisabled }); + diagnostic = new BaseDiagnostic({ isDisabled }); expect(diagnostic.serverUrl).toBe(DIAGNOSTIC_ENDPOINT); expect(diagnostic.isDisabled).toBe(isDisabled); }); test('should set serverUrl to provided value', () => { const serverUrl = 'https://test.com'; - diagnostic = new Diagnostic({ serverUrl }); + diagnostic = new BaseDiagnostic({ serverUrl }); expect(diagnostic.serverUrl).toBe(serverUrl); expect(diagnostic.isDisabled).toBe(false); }); diff --git a/packages/analytics-core/test/index.test.ts b/packages/analytics-core/test/index.test.ts index 4cfceafa7..e61d64e03 100644 --- a/packages/analytics-core/test/index.test.ts +++ b/packages/analytics-core/test/index.test.ts @@ -14,7 +14,7 @@ import { UUID, MemoryStorage, createIdentifyEvent, - Diagnostic, + BaseDiagnostic, buildResult, EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE, MISSING_API_KEY_DIAGNOSTIC_MESSAGE, @@ -40,7 +40,7 @@ describe('index', () => { expect(typeof client.remove).toBe('function'); expect(typeof BaseTransport).toBe('function'); expect(typeof Destination).toBe('function'); - expect(typeof Diagnostic).toBe('function'); + expect(typeof BaseDiagnostic).toBe('function'); expect(typeof Config).toBe('function'); expect(typeof Logger).toBe('function'); expect(typeof returnWrapper).toBe('function'); diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index f382a4ed0..4e7cf7047 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -1,13 +1,5 @@ import { Destination, getResponseBodyString } from '../../src/plugins/destination'; -import { - Config, - DestinationContext, - Logger, - Payload, - Result, - Status, - Diagnostic as IDiagnostic, -} from '@amplitude/analytics-types'; +import { Config, DestinationContext, Logger, Payload, Result, Status, Diagnostic } from '@amplitude/analytics-types'; import { API_KEY, useDefaultConfig } from '../helpers/default'; import { INVALID_API_KEY, @@ -24,12 +16,12 @@ import { } from '../../src/diagnostics/constants'; const jsons = (obj: any) => JSON.stringify(obj, null, 2); -class Diagnostic implements IDiagnostic { +class TestDiagnostic implements Diagnostic { track = jest.fn(); isDisabled = false; serverUrl = 'test'; } -const diagnosticProvider = new Diagnostic(); +const diagnosticProvider = new TestDiagnostic(); describe('destination', () => { afterEach(() => { diff --git a/packages/analytics-node/test/config.test.ts b/packages/analytics-node/test/config.test.ts index d3231ab6a..0a9852bd8 100644 --- a/packages/analytics-node/test/config.test.ts +++ b/packages/analytics-node/test/config.test.ts @@ -27,7 +27,7 @@ describe('config', () => { storageProvider: undefined, transportProvider: new Http(), useBatch: false, - diagnosticProvider: new core.Diagnostic({ apiKey: API_KEY }), + diagnosticProvider: new core.BaseDiagnostic({ apiKey: API_KEY }), }); }); }); @@ -57,7 +57,7 @@ describe('config', () => { transportProvider: new Http(), userId: undefined, useBatch: false, - diagnosticProvider: new core.Diagnostic({ apiKey: API_KEY }), + diagnosticProvider: new core.BaseDiagnostic({ apiKey: API_KEY }), }); }); }); diff --git a/packages/analytics-react-native/test/config.test.ts b/packages/analytics-react-native/test/config.test.ts index 20e6cecf5..c2f8768b8 100644 --- a/packages/analytics-react-native/test/config.test.ts +++ b/packages/analytics-react-native/test/config.test.ts @@ -60,7 +60,7 @@ describe('config', () => { }, transportProvider: new FetchTransport(), useBatch: false, - diagnosticProvider: new core.Diagnostic({ apiKey: '' }), + diagnosticProvider: new core.BaseDiagnostic({ apiKey: '' }), trackingSessionEvents: false, }); }); @@ -114,7 +114,7 @@ describe('config', () => { }, transportProvider: new FetchTransport(), useBatch: false, - diagnosticProvider: new core.Diagnostic({ apiKey: API_KEY }), + diagnosticProvider: new core.BaseDiagnostic({ apiKey: API_KEY }), trackingSessionEvents: false, }); }); @@ -196,7 +196,7 @@ describe('config', () => { }, transportProvider: new FetchTransport(), useBatch: false, - diagnosticProvider: new core.Diagnostic({ apiKey: API_KEY }), + diagnosticProvider: new core.BaseDiagnostic({ apiKey: API_KEY }), _userId: 'userIdFromCookies', }); }); diff --git a/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts b/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts index 336da4dfb..9c5e3e157 100644 --- a/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts +++ b/packages/plugin-page-view-tracking-browser/test/page-view-tracking.test.ts @@ -1,5 +1,5 @@ import { createInstance } from '@amplitude/analytics-browser'; -import { Diagnostic, Logger, UUID } from '@amplitude/analytics-core'; +import { BaseDiagnostic, Logger, UUID } from '@amplitude/analytics-core'; import { BrowserConfig, LogLevel } from '@amplitude/analytics-types'; import { pageViewTrackingPlugin, shouldTrackHistoryPageView } from '../src/page-view-tracking'; import { CookieStorage, FetchTransport } from '@amplitude/analytics-client-common'; @@ -31,7 +31,7 @@ describe('pageViewTrackingPlugin', () => { language: true, platform: true, }, - diagnosticProvider: new Diagnostic(), + diagnosticProvider: new BaseDiagnostic(), }; beforeAll(() => { diff --git a/packages/plugin-web-attribution-browser/test/web-attribution.test.ts b/packages/plugin-web-attribution-browser/test/web-attribution.test.ts index ce81a4d9d..781428595 100644 --- a/packages/plugin-web-attribution-browser/test/web-attribution.test.ts +++ b/packages/plugin-web-attribution-browser/test/web-attribution.test.ts @@ -3,7 +3,7 @@ import { BASE_CAMPAIGN, CampaignParser, CookieStorage, FetchTransport } from '@a import { webAttributionPlugin } from '../src/web-attribution'; import * as helpers from '../src/helpers'; import { BrowserConfig, LogLevel } from '@amplitude/analytics-types'; -import { Diagnostic, Logger, UUID } from '@amplitude/analytics-core'; +import { BaseDiagnostic, Logger, UUID } from '@amplitude/analytics-core'; describe('webAttributionPlugin', () => { const mockConfig: BrowserConfig = { @@ -32,7 +32,7 @@ describe('webAttributionPlugin', () => { language: true, platform: true, }, - diagnosticProvider: new Diagnostic(), + diagnosticProvider: new BaseDiagnostic(), }; describe('setup', () => { From d6bac4c63a04d480da960893134e45548f58f0a7 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 11 Oct 2023 15:58:49 -0700 Subject: [PATCH 32/50] fix: payload and test --- .../analytics-browser/test/diagnostic.test.ts | 34 +++++++++++++++++- packages/analytics-core/src/constants.ts | 1 - .../src/diagnostics/constants.ts | 1 + .../src/diagnostics/diagnostic.ts | 35 +++++++++---------- packages/analytics-core/src/index.ts | 2 ++ .../analytics-core/test/diagnostic.test.ts | 35 +++++++++---------- packages/analytics-core/test/index.test.ts | 4 +++ 7 files changed, 74 insertions(+), 38 deletions(-) diff --git a/packages/analytics-browser/test/diagnostic.test.ts b/packages/analytics-browser/test/diagnostic.test.ts index e51c6ebac..dac75c017 100644 --- a/packages/analytics-browser/test/diagnostic.test.ts +++ b/packages/analytics-browser/test/diagnostic.test.ts @@ -1,15 +1,47 @@ +import { DIAGNOSTIC_ENDPOINT, DIAGNOSTIC_METADATA_TYPE } from '@amplitude/analytics-core'; import { BrowserDiagnostic } from '../src/diagnostics/diagnostic'; describe('Diagnostic', () => { test('should fetch', async () => { const diagnostic = new BrowserDiagnostic(); const fetchMock = jest.fn().mockResolvedValueOnce({} as Response); + Date.now = jest.fn().mockReturnValue(12345); global.fetch = fetchMock; - diagnostic.track(5, 200, 'Test message'); + diagnostic.track(5, 400, 'test message 0'); + diagnostic.track(10, 500, 'test message 1'); await diagnostic.flush(); expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith(DIAGNOSTIC_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + api_key: '', + omni_metrics: [ + { + metadata_type: DIAGNOSTIC_METADATA_TYPE, + library: 'amplitude-ts', + accounting_time_min: 12345, + response_code: 400, + trigger: 'test message 0', + action: 'drop events', + event_count: 5, + }, + { + metadata_type: DIAGNOSTIC_METADATA_TYPE, + library: 'amplitude-ts', + accounting_time_min: 12345, + response_code: 500, + trigger: 'test message 1', + action: 'drop events', + event_count: 10, + }, + ], + }), + }); fetchMock.mockRestore(); }); }); diff --git a/packages/analytics-core/src/constants.ts b/packages/analytics-core/src/constants.ts index c3bbe57b0..4459374b1 100644 --- a/packages/analytics-core/src/constants.ts +++ b/packages/analytics-core/src/constants.ts @@ -5,4 +5,3 @@ export const AMPLITUDE_SERVER_URL = 'https://api2.amplitude.com/2/httpapi'; export const EU_AMPLITUDE_SERVER_URL = 'https://api.eu.amplitude.com/2/httpapi'; export const AMPLITUDE_BATCH_SERVER_URL = 'https://api2.amplitude.com/batch'; export const EU_AMPLITUDE_BATCH_SERVER_URL = 'https://api.eu.amplitude.com/batch'; -export const DIAGNOSTIC_ENDPOINT = 'https://api-omni.stag2.amplitude.com/omni/metrics'; diff --git a/packages/analytics-core/src/diagnostics/constants.ts b/packages/analytics-core/src/diagnostics/constants.ts index f4c3685a1..114a08726 100644 --- a/packages/analytics-core/src/diagnostics/constants.ts +++ b/packages/analytics-core/src/diagnostics/constants.ts @@ -7,3 +7,4 @@ export const PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE = 'payload too large'; export const EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE = 'exceeded daily quota users or devices'; export const DIAGNOSTIC_METADATA_TYPE = 'SDK_DIAGNOSTIC'; +export const DIAGNOSTIC_ENDPOINT = 'https://api-omni.stag2.amplitude.com/omni/metrics'; diff --git a/packages/analytics-core/src/diagnostics/diagnostic.ts b/packages/analytics-core/src/diagnostics/diagnostic.ts index 76a5c1a2d..ae5e1e4e5 100644 --- a/packages/analytics-core/src/diagnostics/diagnostic.ts +++ b/packages/analytics-core/src/diagnostics/diagnostic.ts @@ -1,13 +1,13 @@ import { Diagnostic, DiagnosticOptions } from '@amplitude/analytics-types'; -import { DIAGNOSTIC_ENDPOINT } from '../constants'; -import { DiagnosticEvent } from './typings'; +import { DIAGNOSTIC_ENDPOINT } from '../diagnostics/constants'; +import { DiagnosticOmniMetrics } from './typings'; import { DIAGNOSTIC_METADATA_TYPE } from './constants'; export class BaseDiagnostic implements Diagnostic { isDisabled = false; serverUrl: string = DIAGNOSTIC_ENDPOINT; apiKey?: string = ''; - queue: DiagnosticEvent[] = []; + queue: DiagnosticOmniMetrics[] = []; private scheduled: ReturnType | null = null; // deault delay is 1 minute @@ -46,29 +46,28 @@ export class BaseDiagnostic implements Diagnostic { } } - diagnosticEventBuilder(eventCount: number, code: number, message: string): DiagnosticEvent { + diagnosticEventBuilder(eventCount: number, code: number, message: string): DiagnosticOmniMetrics { return { - api_key: this.apiKey || '', - omni_metrics: { - metadata_type: DIAGNOSTIC_METADATA_TYPE, - library: 'amplitude-ts', - accounting_time_min: Date.now(), - response_code: code, - trigger: message, - action: 'drop events', - event_count: eventCount, - }, + metadata_type: DIAGNOSTIC_METADATA_TYPE, + library: 'amplitude-ts', + accounting_time_min: Date.now(), + response_code: code, + trigger: message, + action: 'drop events', + event_count: eventCount, }; } - requestPayloadBuilder(events: DiagnosticEvent[]): object { + requestPayloadBuilder(events: DiagnosticOmniMetrics[]): object { return { + method: 'POST', headers: { 'Content-Type': 'application/json', - Accept: '*/*', }, - events: events, - method: 'POST', + body: JSON.stringify({ + api_key: this.apiKey || '', + omni_metrics: events, + }), }; } } diff --git a/packages/analytics-core/src/index.ts b/packages/analytics-core/src/index.ts index ce714d81c..3b5828ae6 100644 --- a/packages/analytics-core/src/index.ts +++ b/packages/analytics-core/src/index.ts @@ -11,6 +11,8 @@ export { EVENT_ERROR_DIAGNOSTIC_MESSAGE, PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE, EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE, + DIAGNOSTIC_METADATA_TYPE, + DIAGNOSTIC_ENDPOINT, } from './diagnostics/constants'; export { Config } from './config'; export { Logger } from './logger'; diff --git a/packages/analytics-core/test/diagnostic.test.ts b/packages/analytics-core/test/diagnostic.test.ts index 517eda347..214dcd1d9 100644 --- a/packages/analytics-core/test/diagnostic.test.ts +++ b/packages/analytics-core/test/diagnostic.test.ts @@ -1,4 +1,4 @@ -import { DIAGNOSTIC_ENDPOINT } from '../src/constants'; +import { DIAGNOSTIC_ENDPOINT, DIAGNOSTIC_METADATA_TYPE } from '../src/diagnostics/constants'; import { BaseDiagnostic } from '../src/diagnostics/diagnostic'; jest.useFakeTimers(); @@ -43,10 +43,10 @@ describe('Diagnostic', () => { diagnostic.track(eventCount, code, 'Test message'); expect(diagnostic.queue).toHaveLength(1); - expect(diagnostic.queue[0].omni_metrics.event_count).toBe(eventCount); - expect(diagnostic.queue[0].omni_metrics.response_code).toBe(code); - expect(diagnostic.queue[0].omni_metrics.trigger).toBe('Test message'); - expect(diagnostic.queue[0].omni_metrics.library).toBe('amplitude-ts'); + expect(diagnostic.queue[0].event_count).toBe(eventCount); + expect(diagnostic.queue[0].response_code).toBe(code); + expect(diagnostic.queue[0].trigger).toBe('Test message'); + expect(diagnostic.queue[0].library).toBe('amplitude-ts'); }); test('should not add to queen when disabled', () => { @@ -86,26 +86,25 @@ describe('Diagnostic', () => { test('should return correct payload', () => { const events = [ { - api_key: 'test-api-key', - omni_metrics: { - metadata_type: 'diagnostic', - library: 'diagnostic-test-library', - accounting_time_min: Date.now(), - response_code: code, - trigger: 'test trigger', - action: 'test action', - event_count: eventCount, - }, + metadata_type: DIAGNOSTIC_METADATA_TYPE, + library: 'amplitude-ts', + accounting_time_min: Date.now(), + response_code: code, + trigger: 'test trigger', + action: 'test action', + event_count: eventCount, }, ]; const expectedPayload = { + method: 'POST', headers: { 'Content-Type': 'application/json', - Accept: '*/*', }, - events: events, - method: 'POST', + body: JSON.stringify({ + api_key: '', + omni_metrics: events, + }), }; expect(diagnostic.requestPayloadBuilder(events)).toEqual(expectedPayload); diff --git a/packages/analytics-core/test/index.test.ts b/packages/analytics-core/test/index.test.ts index e61d64e03..792c017ad 100644 --- a/packages/analytics-core/test/index.test.ts +++ b/packages/analytics-core/test/index.test.ts @@ -23,6 +23,8 @@ import { EVENT_ERROR_DIAGNOSTIC_MESSAGE, PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE, EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE, + DIAGNOSTIC_METADATA_TYPE, + DIAGNOSTIC_ENDPOINT, } from '../src/index'; describe('index', () => { @@ -60,5 +62,7 @@ describe('index', () => { expect(EVENT_ERROR_DIAGNOSTIC_MESSAGE).toBe('event error'); expect(PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE).toBe('payload too large'); expect(EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE).toBe('exceeded daily quota users or devices'); + expect(DIAGNOSTIC_METADATA_TYPE).toBe('SDK_DIAGNOSTIC'); + expect(DIAGNOSTIC_ENDPOINT).toBe('https://api-omni.stag2.amplitude.com/omni/metrics'); }); }); From 7cc0639f6a49a528837f17db90fa3b11d938cd8f Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 11 Oct 2023 16:37:14 -0700 Subject: [PATCH 33/50] refactor: rename to DiagnosticRequest --- packages/analytics-core/src/diagnostics/diagnostic.ts | 4 ++-- packages/analytics-core/src/diagnostics/typings.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/analytics-core/src/diagnostics/diagnostic.ts b/packages/analytics-core/src/diagnostics/diagnostic.ts index ae5e1e4e5..0ac7fe0d0 100644 --- a/packages/analytics-core/src/diagnostics/diagnostic.ts +++ b/packages/analytics-core/src/diagnostics/diagnostic.ts @@ -27,7 +27,7 @@ export class BaseDiagnostic implements Diagnostic { return; } - this.queue.push(this.diagnosticEventBuilder(eventCount, code, message)); + this.queue.push(this.diagnosticRequestBuilder(eventCount, code, message)); if (!this.scheduled) { this.scheduled = setTimeout(() => { @@ -46,7 +46,7 @@ export class BaseDiagnostic implements Diagnostic { } } - diagnosticEventBuilder(eventCount: number, code: number, message: string): DiagnosticOmniMetrics { + diagnosticRequestBuilder(eventCount: number, code: number, message: string): DiagnosticOmniMetrics { return { metadata_type: DIAGNOSTIC_METADATA_TYPE, library: 'amplitude-ts', diff --git a/packages/analytics-core/src/diagnostics/typings.ts b/packages/analytics-core/src/diagnostics/typings.ts index e5fc5fbe2..17e628f7d 100644 --- a/packages/analytics-core/src/diagnostics/typings.ts +++ b/packages/analytics-core/src/diagnostics/typings.ts @@ -8,7 +8,7 @@ export interface DiagnosticOmniMetrics { event_count: number; } -export interface DiagnosticEvent { +export interface DiagnosticRequest { api_key: string; omni_metrics: DiagnosticOmniMetrics; } From 6cb202b93a7907a1b09143858bdeb68cc25ab73e Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 11 Oct 2023 17:23:55 -0700 Subject: [PATCH 34/50] feat: diagnostic provider only accepts Diagnostic --- packages/analytics-browser/src/config.ts | 3 +- .../test/browser-client.test.ts | 14 ++++----- .../analytics-browser/test/config.test.ts | 8 ++--- packages/analytics-core/src/config.ts | 7 ++--- .../src/diagnostics/diagnostic.ts | 4 +-- .../analytics-core/src/plugins/destination.ts | 31 +++++++------------ packages/analytics-core/test/config.test.ts | 4 +-- .../analytics-core/test/diagnostic.test.ts | 10 ++++++ packages/analytics-types/src/config/core.ts | 4 +-- 9 files changed, 40 insertions(+), 45 deletions(-) diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index b6281bda6..58bb9ce8e 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -14,7 +14,6 @@ import { IngestionMetadata, IdentityStorageType, ServerZoneType, - DiagnosticOptions, } from '@amplitude/analytics-types'; import { Config, Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; import { CookieStorage, getCookieName, FetchTransport, getQueryParams } from '@amplitude/analytics-client-common'; @@ -78,7 +77,7 @@ export class BrowserConfig extends Config implements IBrowserConfig { }, public transport: 'fetch' | 'xhr' | 'beacon' = 'fetch', public useBatch: boolean = false, - public diagnosticProvider: Diagnostic | DiagnosticOptions = new BrowserDiagnostic(), + public diagnosticProvider: Diagnostic = new BrowserDiagnostic(), userId?: string, ) { super({ apiKey, storageProvider, transportProvider: createTransport(transport) }); diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index 1f9b8c16e..1ca7b632a 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -2,7 +2,7 @@ import { AmplitudeBrowser } from '../src/browser-client'; import * as core from '@amplitude/analytics-core'; import * as Config from '../src/config'; import * as CookieMigration from '../src/cookie-migration'; -import { Diagnostic, Status, UserSession } from '@amplitude/analytics-types'; +import { Status, UserSession } from '@amplitude/analytics-types'; import { CookieStorage, FetchTransport, @@ -276,7 +276,7 @@ describe('browser-client', () => { await client.init(apiKey, { defaultTracking: false, }).promise; - const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider, 'track'); client.config.transportProvider = transportProvider; await client.track('event_type', { userId: 'user_0' }).promise; @@ -293,7 +293,7 @@ describe('browser-client', () => { await client.init(apiKey, { defaultTracking: false, }).promise; - const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider, 'track'); client.config.transportProvider = transportProvider; await client.track('event_type', { userId: 'user_0' }).promise; @@ -327,7 +327,7 @@ describe('browser-client', () => { await client.init(apiKey, { defaultTracking: false, }).promise; - const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider, 'track'); client.config.transportProvider = transportProvider; await client.track('event_type', { userId: 'user_0' }).promise; @@ -354,7 +354,7 @@ describe('browser-client', () => { await client.init(apiKey, { defaultTracking: false, }).promise; - const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider, 'track'); client.config.transportProvider = transportProvider; client.track('event_type', { userId: 'user_0' }); // flush() calls destination.flush(useRetry: false) @@ -380,7 +380,7 @@ describe('browser-client', () => { await client.init(apiKey, { defaultTracking: false, }).promise; - const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider, 'track'); client.config.transportProvider = transportProvider; await client.track('event_type', { userId: 'user_0' }).promise; @@ -410,7 +410,7 @@ describe('browser-client', () => { defaultTracking: false, flushMaxRetries: 1, }).promise; - const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider as Diagnostic, 'track'); + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider, 'track'); client.config.transportProvider = transportProvider; await client.track('event_type', { userId: 'user_0' }).promise; diff --git a/packages/analytics-browser/test/config.test.ts b/packages/analytics-browser/test/config.test.ts index 30f20ac81..3935c45b3 100644 --- a/packages/analytics-browser/test/config.test.ts +++ b/packages/analytics-browser/test/config.test.ts @@ -11,6 +11,7 @@ import { SendBeaconTransport } from '../src/transports/send-beacon'; import { uuidPattern } from './helpers/constants'; import { DEFAULT_IDENTITY_STORAGE, DEFAULT_SERVER_ZONE } from '../src/constants'; import { AmplitudeBrowser } from '../src/browser-client'; +import { BrowserDiagnostic } from '../src/diagnostics/diagnostic'; describe('config', () => { const someUUID: string = expect.stringMatching(uuidPattern) as string; @@ -20,7 +21,6 @@ describe('config', () => { const someLocalStorage: LocalStorageModule.LocalStorage = expect.any( LocalStorageModule.LocalStorage, ) as LocalStorageModule.LocalStorage; - const someDiagnosticProvider: core.BaseDiagnostic = expect.any(core.BaseDiagnostic) as core.BaseDiagnostic; let apiKey = ''; @@ -74,7 +74,7 @@ describe('config', () => { transport: 'fetch', transportProvider: new FetchTransport(), useBatch: false, - diagnosticProvider: someDiagnosticProvider, + diagnosticProvider: new BrowserDiagnostic(), }); }); @@ -130,7 +130,7 @@ describe('config', () => { transport: 'fetch', transportProvider: new FetchTransport(), useBatch: false, - diagnosticProvider: someDiagnosticProvider, + diagnosticProvider: new BrowserDiagnostic(), }); expect(getTopLevelDomain).toHaveBeenCalledTimes(1); }); @@ -216,7 +216,7 @@ describe('config', () => { transport: 'fetch', transportProvider: new FetchTransport(), useBatch: false, - diagnosticProvider: someDiagnosticProvider, + diagnosticProvider: new BrowserDiagnostic(), }); }); diff --git a/packages/analytics-core/src/config.ts b/packages/analytics-core/src/config.ts index 565f64556..c5c1579bc 100644 --- a/packages/analytics-core/src/config.ts +++ b/packages/analytics-core/src/config.ts @@ -2,7 +2,6 @@ import { Event, Config as IConfig, Diagnostic, - DiagnosticOptions, Logger as ILogger, LogLevel, Storage, @@ -52,7 +51,7 @@ export class Config implements IConfig { transportProvider: Transport; storageProvider?: Storage; useBatch: boolean; - diagnosticProvider: Diagnostic | DiagnosticOptions; + diagnosticProvider: Diagnostic; protected _optOut = false; get optOut() { @@ -83,10 +82,8 @@ export class Config implements IConfig { if (options.diagnosticProvider == undefined) { this.diagnosticProvider = defaultConfig.diagnosticProvider; - } else if (options.diagnosticProvider instanceof BaseDiagnostic) { - this.diagnosticProvider = options.diagnosticProvider; } else { - this.diagnosticProvider = new BaseDiagnostic(options.diagnosticProvider as DiagnosticOptions); + this.diagnosticProvider = options.diagnosticProvider; } this.diagnosticProvider.apiKey = this.apiKey; diff --git a/packages/analytics-core/src/diagnostics/diagnostic.ts b/packages/analytics-core/src/diagnostics/diagnostic.ts index 0ac7fe0d0..724b6c8db 100644 --- a/packages/analytics-core/src/diagnostics/diagnostic.ts +++ b/packages/analytics-core/src/diagnostics/diagnostic.ts @@ -17,9 +17,7 @@ export class BaseDiagnostic implements Diagnostic { constructor(options?: DiagnosticOptions) { this.isDisabled = options && options.isDisabled ? options.isDisabled : false; this.serverUrl = options && options.serverUrl ? options.serverUrl : DIAGNOSTIC_ENDPOINT; - if (options && options.apiKey) { - this.apiKey = options.apiKey; - } + this.apiKey = options && options.apiKey ? options.apiKey : ''; } track(eventCount: number, code: number, message: string) { diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index 8b584bf64..ee4c7810d 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -2,7 +2,6 @@ import { Config, DestinationContext as Context, DestinationPlugin, - Diagnostic, Event, InvalidResponse, PayloadTooLargeResponse, @@ -96,7 +95,7 @@ export class Destination implements DestinationPlugin { return true; } void this.fulfillRequest([context], 500, MAX_RETRIES_EXCEEDED_MESSAGE); - (this.config.diagnosticProvider as Diagnostic).track(list.length, 500, EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 500, EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE); return false; }); @@ -144,7 +143,7 @@ export class Destination implements DestinationPlugin { async send(list: Context[], useRetry = true) { if (!this.config.apiKey) { - (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, MISSING_API_KEY_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 400, MISSING_API_KEY_DIAGNOSTIC_MESSAGE); return this.fulfillRequest(list, 400, MISSING_API_KEY_MESSAGE); } @@ -165,7 +164,7 @@ export class Destination implements DestinationPlugin { const { serverUrl } = createServerConfig(this.config.serverUrl, this.config.serverZone, this.config.useBatch); const res = await this.config.transportProvider.send(serverUrl, payload); if (res === null) { - (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); this.fulfillRequest(list, 0, UNEXPECTED_ERROR_MESSAGE); return; } @@ -174,7 +173,7 @@ export class Destination implements DestinationPlugin { const errorMessage = getErrorMessage(e); this.config.loggerProvider.error(errorMessage); this.fulfillRequest(list, 0, errorMessage); - (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); } } @@ -215,11 +214,7 @@ export class Destination implements DestinationPlugin { handleInvalidResponse(res: InvalidResponse, list: Context[], useRetry: boolean) { if (res.body.missingField || res.body.error.startsWith(INVALID_API_KEY)) { this.fulfillRequest(list, res.statusCode, `${res.status}: ${getResponseBodyString(res)}`); - (this.config.diagnosticProvider as Diagnostic).track( - list.length, - 400, - INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE, - ); + this.config.diagnosticProvider.track(list.length, 400, INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE); return; } @@ -233,7 +228,7 @@ export class Destination implements DestinationPlugin { if (useRetry) { if (dropIndexSet.size) { - (this.config.diagnosticProvider as Diagnostic).track(dropIndexSet.size, 400, EVENT_ERROR_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(dropIndexSet.size, 400, EVENT_ERROR_DIAGNOSTIC_MESSAGE); } const retry = list.filter((context, index) => { if (dropIndexSet.has(index)) { @@ -249,13 +244,13 @@ export class Destination implements DestinationPlugin { } this.addToQueue(...retry); } else { - (this.config.diagnosticProvider as Diagnostic).track(list.length, 400, EVENT_ERROR_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 400, EVENT_ERROR_DIAGNOSTIC_MESSAGE); } } handlePayloadTooLargeResponse(res: PayloadTooLargeResponse, list: Context[], useRetry: boolean) { if (list.length === 1 || !useRetry) { - (this.config.diagnosticProvider as Diagnostic).track(list.length, 413, PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 413, PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE); this.fulfillRequest(list, res.statusCode, res.body.error); return; } @@ -269,7 +264,7 @@ export class Destination implements DestinationPlugin { handleRateLimitResponse(res: RateLimitResponse, list: Context[], useRetry: boolean) { if (!useRetry) { - (this.config.diagnosticProvider as Diagnostic).track(list.length, 429, EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 429, EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE); this.fulfillRequest(list, res.statusCode, res.status); return; } @@ -297,11 +292,7 @@ export class Destination implements DestinationPlugin { const dropEvents = retry.filter((element) => !retry.includes(element)); if (dropEvents.length > 0) { - (this.config.diagnosticProvider as Diagnostic).track( - dropEvents.length, - 429, - EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE, - ); + this.config.diagnosticProvider.track(dropEvents.length, 429, EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE); } if (retry.length > 0) { @@ -322,7 +313,7 @@ export class Destination implements DestinationPlugin { ); } else { this.fulfillRequest(list, res.statusCode, res.status); - (this.config.diagnosticProvider as Diagnostic).track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); } } diff --git a/packages/analytics-core/test/config.test.ts b/packages/analytics-core/test/config.test.ts index 9f00a4acc..e9d2c9723 100644 --- a/packages/analytics-core/test/config.test.ts +++ b/packages/analytics-core/test/config.test.ts @@ -55,7 +55,7 @@ describe('config', () => { storageProvider: defaultConfig.storageProvider, transportProvider: defaultConfig.transportProvider, useBatch: true, - diagnosticProvider: { isDisabled: true }, + diagnosticProvider: defaultConfig.diagnosticProvider, }); expect(config).toEqual({ apiKey: 'apiKey', @@ -79,7 +79,7 @@ describe('config', () => { storageProvider: defaultConfig.storageProvider, transportProvider: defaultConfig.transportProvider, useBatch: true, - diagnosticProvider: new BaseDiagnostic({ isDisabled: true, apiKey: API_KEY }), + diagnosticProvider: defaultConfig.diagnosticProvider, }); }); diff --git a/packages/analytics-core/test/diagnostic.test.ts b/packages/analytics-core/test/diagnostic.test.ts index 214dcd1d9..b07c2e0f6 100644 --- a/packages/analytics-core/test/diagnostic.test.ts +++ b/packages/analytics-core/test/diagnostic.test.ts @@ -28,6 +28,7 @@ describe('Diagnostic', () => { diagnostic = new BaseDiagnostic({ isDisabled }); expect(diagnostic.serverUrl).toBe(DIAGNOSTIC_ENDPOINT); expect(diagnostic.isDisabled).toBe(isDisabled); + expect(diagnostic.apiKey).toBe(''); }); test('should set serverUrl to provided value', () => { @@ -35,6 +36,15 @@ describe('Diagnostic', () => { diagnostic = new BaseDiagnostic({ serverUrl }); expect(diagnostic.serverUrl).toBe(serverUrl); expect(diagnostic.isDisabled).toBe(false); + expect(diagnostic.apiKey).toBe(''); + }); + + test('should set apiKey to provided value', () => { + const apiKey = '12345'; + diagnostic = new BaseDiagnostic({ apiKey }); + expect(diagnostic.serverUrl).toBe(DIAGNOSTIC_ENDPOINT); + expect(diagnostic.isDisabled).toBe(false); + expect(diagnostic.apiKey).toBe('12345'); }); }); diff --git a/packages/analytics-types/src/config/core.ts b/packages/analytics-types/src/config/core.ts index 1a86d730f..a54251726 100644 --- a/packages/analytics-types/src/config/core.ts +++ b/packages/analytics-types/src/config/core.ts @@ -5,7 +5,7 @@ import { ServerZoneType } from '../server-zone'; import { Storage } from '../storage'; import { Transport } from '../transport'; import { Logger, LogLevel } from '../logger'; -import { Diagnostic, DiagnosticOptions } from '../diagnostic'; +import { Diagnostic } from '../diagnostic'; export interface Config { apiKey: string; @@ -24,7 +24,7 @@ export interface Config { storageProvider?: Storage; transportProvider: Transport; useBatch: boolean; - diagnosticProvider: Diagnostic | DiagnosticOptions; + diagnosticProvider: Diagnostic; } export interface Options extends Partial { From ef5706f7dc39b49dbb2ef77e3c2a166bb3d66544 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 11 Oct 2023 18:58:15 -0700 Subject: [PATCH 35/50] test: mock diagnosticProvider --- packages/analytics-browser/test/helpers/mock.ts | 8 ++++++-- packages/analytics-core/test/config.test.ts | 1 + packages/analytics-core/test/helpers/default.ts | 10 +++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/analytics-browser/test/helpers/mock.ts b/packages/analytics-browser/test/helpers/mock.ts index 073ed8d68..6477fb34e 100644 --- a/packages/analytics-browser/test/helpers/mock.ts +++ b/packages/analytics-browser/test/helpers/mock.ts @@ -1,6 +1,5 @@ import { Logger, MemoryStorage, UUID } from '@amplitude/analytics-core'; import { BrowserClient, BrowserConfig, LogLevel, UserSession } from '@amplitude/analytics-types'; -import { BrowserDiagnostic } from '../../src/diagnostics/diagnostic'; export const createAmplitudeMock = (): jest.MockedObject => ({ init: jest.fn(), @@ -79,7 +78,12 @@ export const createConfigurationMock = (options?: Partial) => { }, trackingSessionEvents: false, userId: undefined, - diagnosticProvider: new BrowserDiagnostic(), + diagnosticProvider: { + isDisabled: false, + serverUrl: undefined, + apiKey: undefined, + track: jest.fn(), + }, ...options, }; }; diff --git a/packages/analytics-core/test/config.test.ts b/packages/analytics-core/test/config.test.ts index e9d2c9723..128091607 100644 --- a/packages/analytics-core/test/config.test.ts +++ b/packages/analytics-core/test/config.test.ts @@ -17,6 +17,7 @@ describe('config', () => { apiKey: API_KEY, storageProvider: defaultConfig.storageProvider, transportProvider: defaultConfig.transportProvider, + diagnosticProvider: defaultConfig.diagnosticProvider, }); expect(config).toEqual({ apiKey: 'apiKey', diff --git a/packages/analytics-core/test/helpers/default.ts b/packages/analytics-core/test/helpers/default.ts index 2870a2e42..5782d940e 100644 --- a/packages/analytics-core/test/helpers/default.ts +++ b/packages/analytics-core/test/helpers/default.ts @@ -2,8 +2,6 @@ import { Config } from '@amplitude/analytics-types'; import { getDefaultConfig } from '../../src/config'; export const useDefaultConfig = (): Config => { - const defaultConfig = getDefaultConfig(); - defaultConfig.diagnosticProvider.apiKey = API_KEY; return { apiKey: API_KEY, transportProvider: { @@ -17,7 +15,13 @@ export const useDefaultConfig = (): Config => { reset: async () => undefined, getRaw: async () => undefined, }, - ...defaultConfig, + ...getDefaultConfig(), + diagnosticProvider: { + isDisabled: false, + serverUrl: undefined, + apiKey: undefined, + track: jest.fn(), + }, }; }; From 1d252d2a7adb302b812be8fc865ddae6588a659b Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Thu, 12 Oct 2023 10:27:17 -0700 Subject: [PATCH 36/50] refactor: extract DIAGNOSTIC_MESSAGES --- .../test/browser-client.test.ts | 12 +++---- .../src/diagnostics/constants.ts | 18 ++++++----- packages/analytics-core/src/index.ts | 12 +------ .../analytics-core/src/plugins/destination.ts | 32 +++++++------------ packages/analytics-core/test/index.test.ts | 22 +++++-------- .../test/plugins/destination.test.ts | 20 ++++-------- 6 files changed, 44 insertions(+), 72 deletions(-) diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index 1ca7b632a..2dc7c94f9 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -298,12 +298,12 @@ describe('browser-client', () => { await client.track('event_type', { userId: 'user_0' }).promise; expect(diagnosticTrack).toHaveBeenCalledTimes(1); - expect(diagnosticTrack).toHaveBeenCalledWith(1, 0, core.UNEXPECTED_DIAGNOSTIC_MESSAGE); + expect(diagnosticTrack).toHaveBeenCalledWith(1, 0, core.DIAGNOSTIC_MESSAGES.UNEXPECTED_ERROR); }); test.each([ - ['api_key', undefined, core.INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE], - [undefined, { time: [0] }, core.EVENT_ERROR_DIAGNOSTIC_MESSAGE], + ['api_key', undefined, core.DIAGNOSTIC_MESSAGES.INVALID_OR_MISSING_FIELDS], + [undefined, { time: [0] }, core.DIAGNOSTIC_MESSAGES.EVENT_ERROR], ])( 'should diagnostic track when 400 invalid response', async (missingField, eventsWithInvalidFields, message) => { @@ -361,7 +361,7 @@ describe('browser-client', () => { await client.flush().promise; expect(diagnosticTrack).toHaveBeenCalledTimes(1); - expect(diagnosticTrack).toHaveBeenCalledWith(1, 429, core.EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE); + expect(diagnosticTrack).toHaveBeenCalledWith(1, 429, core.DIAGNOSTIC_MESSAGES.EXCEEDED_DAILY_QUOTA); }); test('should diagnostic track when 413', async () => { @@ -385,7 +385,7 @@ describe('browser-client', () => { await client.track('event_type', { userId: 'user_0' }).promise; expect(diagnosticTrack).toHaveBeenCalledTimes(1); - expect(diagnosticTrack).toHaveBeenCalledWith(1, 413, core.PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE); + expect(diagnosticTrack).toHaveBeenCalledWith(1, 413, core.DIAGNOSTIC_MESSAGES.PAYLOAD_TOO_LARGE); }); test('should diagnostic track when 500 hit max retries', async () => { @@ -415,7 +415,7 @@ describe('browser-client', () => { await client.track('event_type', { userId: 'user_0' }).promise; expect(diagnosticTrack).toHaveBeenCalledTimes(1); - expect(diagnosticTrack).toHaveBeenCalledWith(1, 500, core.EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE); + expect(diagnosticTrack).toHaveBeenCalledWith(1, 500, core.DIAGNOSTIC_MESSAGES.EXCEEDED_MAX_RETRY); }); }); }); diff --git a/packages/analytics-core/src/diagnostics/constants.ts b/packages/analytics-core/src/diagnostics/constants.ts index 114a08726..345ee176e 100644 --- a/packages/analytics-core/src/diagnostics/constants.ts +++ b/packages/analytics-core/src/diagnostics/constants.ts @@ -1,10 +1,12 @@ -export const EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE = 'exceeded max retries'; -export const MISSING_API_KEY_DIAGNOSTIC_MESSAGE = 'missing API key'; -export const UNEXPECTED_DIAGNOSTIC_MESSAGE = 'unexpected error'; -export const INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE = 'invalid or missing fields'; -export const EVENT_ERROR_DIAGNOSTIC_MESSAGE = 'event error'; -export const PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE = 'payload too large'; -export const EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE = 'exceeded daily quota users or devices'; - export const DIAGNOSTIC_METADATA_TYPE = 'SDK_DIAGNOSTIC'; export const DIAGNOSTIC_ENDPOINT = 'https://api-omni.stag2.amplitude.com/omni/metrics'; + +export const DIAGNOSTIC_MESSAGES = { + EXCEEDED_MAX_RETRY: 'exceeded max retries', + MISSING_API_KEY: 'missing API key', + UNEXPECTED_ERROR: 'unexpected error', + INVALID_OR_MISSING_FIELDS: 'invalid or missing fields', + EVENT_ERROR: 'event error', + PAYLOAD_TOO_LARGE: 'payload too large', + EXCEEDED_DAILY_QUOTA: 'exceeded daily quota users or devices', +}; diff --git a/packages/analytics-core/src/index.ts b/packages/analytics-core/src/index.ts index 3b5828ae6..319540bb3 100644 --- a/packages/analytics-core/src/index.ts +++ b/packages/analytics-core/src/index.ts @@ -3,17 +3,7 @@ export { Identify } from './identify'; export { Revenue } from './revenue'; export { Destination } from './plugins/destination'; export { BaseDiagnostic } from './diagnostics/diagnostic'; -export { - EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE, - MISSING_API_KEY_DIAGNOSTIC_MESSAGE, - UNEXPECTED_DIAGNOSTIC_MESSAGE, - INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE, - EVENT_ERROR_DIAGNOSTIC_MESSAGE, - PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE, - EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE, - DIAGNOSTIC_METADATA_TYPE, - DIAGNOSTIC_ENDPOINT, -} from './diagnostics/constants'; +export { DIAGNOSTIC_MESSAGES, DIAGNOSTIC_METADATA_TYPE, DIAGNOSTIC_ENDPOINT } from './diagnostics/constants'; export { Config } from './config'; export { Logger } from './logger'; export { AMPLITUDE_PREFIX, STORAGE_PREFIX } from './constants'; diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index ee4c7810d..ebca62d9f 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -22,15 +22,7 @@ import { STORAGE_PREFIX } from '../constants'; import { chunk } from '../utils/chunk'; import { buildResult } from '../utils/result-builder'; import { createServerConfig } from '../config'; -import { - EVENT_ERROR_DIAGNOSTIC_MESSAGE, - EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE, - EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE, - INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE, - MISSING_API_KEY_DIAGNOSTIC_MESSAGE, - PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE, - UNEXPECTED_DIAGNOSTIC_MESSAGE, -} from '../diagnostics/constants'; +import { DIAGNOSTIC_MESSAGES } from '../diagnostics/constants'; function getErrorMessage(error: unknown) { if (error instanceof Error) return error.message; @@ -95,7 +87,7 @@ export class Destination implements DestinationPlugin { return true; } void this.fulfillRequest([context], 500, MAX_RETRIES_EXCEEDED_MESSAGE); - this.config.diagnosticProvider.track(list.length, 500, EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 500, DIAGNOSTIC_MESSAGES.EXCEEDED_MAX_RETRY); return false; }); @@ -143,7 +135,7 @@ export class Destination implements DestinationPlugin { async send(list: Context[], useRetry = true) { if (!this.config.apiKey) { - this.config.diagnosticProvider.track(list.length, 400, MISSING_API_KEY_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 400, DIAGNOSTIC_MESSAGES.MISSING_API_KEY); return this.fulfillRequest(list, 400, MISSING_API_KEY_MESSAGE); } @@ -164,7 +156,7 @@ export class Destination implements DestinationPlugin { const { serverUrl } = createServerConfig(this.config.serverUrl, this.config.serverZone, this.config.useBatch); const res = await this.config.transportProvider.send(serverUrl, payload); if (res === null) { - this.config.diagnosticProvider.track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 0, DIAGNOSTIC_MESSAGES.UNEXPECTED_ERROR); this.fulfillRequest(list, 0, UNEXPECTED_ERROR_MESSAGE); return; } @@ -173,7 +165,7 @@ export class Destination implements DestinationPlugin { const errorMessage = getErrorMessage(e); this.config.loggerProvider.error(errorMessage); this.fulfillRequest(list, 0, errorMessage); - this.config.diagnosticProvider.track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 0, DIAGNOSTIC_MESSAGES.UNEXPECTED_ERROR); } } @@ -214,7 +206,7 @@ export class Destination implements DestinationPlugin { handleInvalidResponse(res: InvalidResponse, list: Context[], useRetry: boolean) { if (res.body.missingField || res.body.error.startsWith(INVALID_API_KEY)) { this.fulfillRequest(list, res.statusCode, `${res.status}: ${getResponseBodyString(res)}`); - this.config.diagnosticProvider.track(list.length, 400, INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 400, DIAGNOSTIC_MESSAGES.INVALID_OR_MISSING_FIELDS); return; } @@ -228,7 +220,7 @@ export class Destination implements DestinationPlugin { if (useRetry) { if (dropIndexSet.size) { - this.config.diagnosticProvider.track(dropIndexSet.size, 400, EVENT_ERROR_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(dropIndexSet.size, 400, DIAGNOSTIC_MESSAGES.EVENT_ERROR); } const retry = list.filter((context, index) => { if (dropIndexSet.has(index)) { @@ -244,13 +236,13 @@ export class Destination implements DestinationPlugin { } this.addToQueue(...retry); } else { - this.config.diagnosticProvider.track(list.length, 400, EVENT_ERROR_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 400, DIAGNOSTIC_MESSAGES.EVENT_ERROR); } } handlePayloadTooLargeResponse(res: PayloadTooLargeResponse, list: Context[], useRetry: boolean) { if (list.length === 1 || !useRetry) { - this.config.diagnosticProvider.track(list.length, 413, PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 413, DIAGNOSTIC_MESSAGES.PAYLOAD_TOO_LARGE); this.fulfillRequest(list, res.statusCode, res.body.error); return; } @@ -264,7 +256,7 @@ export class Destination implements DestinationPlugin { handleRateLimitResponse(res: RateLimitResponse, list: Context[], useRetry: boolean) { if (!useRetry) { - this.config.diagnosticProvider.track(list.length, 429, EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 429, DIAGNOSTIC_MESSAGES.EXCEEDED_DAILY_QUOTA); this.fulfillRequest(list, res.statusCode, res.status); return; } @@ -292,7 +284,7 @@ export class Destination implements DestinationPlugin { const dropEvents = retry.filter((element) => !retry.includes(element)); if (dropEvents.length > 0) { - this.config.diagnosticProvider.track(dropEvents.length, 429, EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(dropEvents.length, 429, DIAGNOSTIC_MESSAGES.EXCEEDED_DAILY_QUOTA); } if (retry.length > 0) { @@ -313,7 +305,7 @@ export class Destination implements DestinationPlugin { ); } else { this.fulfillRequest(list, res.statusCode, res.status); - this.config.diagnosticProvider.track(list.length, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); + this.config.diagnosticProvider.track(list.length, 0, DIAGNOSTIC_MESSAGES.UNEXPECTED_ERROR); } } diff --git a/packages/analytics-core/test/index.test.ts b/packages/analytics-core/test/index.test.ts index 792c017ad..9829a653f 100644 --- a/packages/analytics-core/test/index.test.ts +++ b/packages/analytics-core/test/index.test.ts @@ -16,13 +16,7 @@ import { createIdentifyEvent, BaseDiagnostic, buildResult, - EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE, - MISSING_API_KEY_DIAGNOSTIC_MESSAGE, - UNEXPECTED_DIAGNOSTIC_MESSAGE, - INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE, - EVENT_ERROR_DIAGNOSTIC_MESSAGE, - PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE, - EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE, + DIAGNOSTIC_MESSAGES, DIAGNOSTIC_METADATA_TYPE, DIAGNOSTIC_ENDPOINT, } from '../src/index'; @@ -55,13 +49,13 @@ describe('index', () => { expect(typeof buildResult).toBe('function'); expect(AMPLITUDE_PREFIX).toBe('AMP'); expect(STORAGE_PREFIX).toBe('AMP_unsent'); - expect(EXCEEDED_MAX_RETRY_DIAGNOSTIC_MESSAGE).toBe('exceeded max retries'); - expect(MISSING_API_KEY_DIAGNOSTIC_MESSAGE).toBe('missing API key'); - expect(UNEXPECTED_DIAGNOSTIC_MESSAGE).toBe('unexpected error'); - expect(INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE).toBe('invalid or missing fields'); - expect(EVENT_ERROR_DIAGNOSTIC_MESSAGE).toBe('event error'); - expect(PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE).toBe('payload too large'); - expect(EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE).toBe('exceeded daily quota users or devices'); + expect(DIAGNOSTIC_MESSAGES.EXCEEDED_MAX_RETRY).toBe('exceeded max retries'); + expect(DIAGNOSTIC_MESSAGES.MISSING_API_KEY).toBe('missing API key'); + expect(DIAGNOSTIC_MESSAGES.UNEXPECTED_ERROR).toBe('unexpected error'); + expect(DIAGNOSTIC_MESSAGES.INVALID_OR_MISSING_FIELDS).toBe('invalid or missing fields'); + expect(DIAGNOSTIC_MESSAGES.EVENT_ERROR).toBe('event error'); + expect(DIAGNOSTIC_MESSAGES.PAYLOAD_TOO_LARGE).toBe('payload too large'); + expect(DIAGNOSTIC_MESSAGES.EXCEEDED_DAILY_QUOTA).toBe('exceeded daily quota users or devices'); expect(DIAGNOSTIC_METADATA_TYPE).toBe('SDK_DIAGNOSTIC'); expect(DIAGNOSTIC_ENDPOINT).toBe('https://api-omni.stag2.amplitude.com/omni/metrics'); }); diff --git a/packages/analytics-core/test/plugins/destination.test.ts b/packages/analytics-core/test/plugins/destination.test.ts index 4e7cf7047..381045ed4 100644 --- a/packages/analytics-core/test/plugins/destination.test.ts +++ b/packages/analytics-core/test/plugins/destination.test.ts @@ -7,13 +7,7 @@ import { SUCCESS_MESSAGE, UNEXPECTED_ERROR_MESSAGE, } from '../../src/messages'; -import { - EVENT_ERROR_DIAGNOSTIC_MESSAGE, - EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE, - INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE, - PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE, - UNEXPECTED_DIAGNOSTIC_MESSAGE, -} from '../../src/diagnostics/constants'; +import { DIAGNOSTIC_MESSAGES } from '../../src/diagnostics/constants'; const jsons = (obj: any) => JSON.stringify(obj, null, 2); class TestDiagnostic implements Diagnostic { @@ -495,13 +489,13 @@ describe('destination', () => { await destination.send([context]); expect(callback).toHaveBeenCalledTimes(1); expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); - expect(diagnosticProvider.track).toHaveBeenLastCalledWith(1, 0, UNEXPECTED_DIAGNOSTIC_MESSAGE); + expect(diagnosticProvider.track).toHaveBeenLastCalledWith(1, 0, DIAGNOSTIC_MESSAGES.UNEXPECTED_ERROR); }); test.each([ - ['api_key', undefined, true, 2, INVALID_OR_MISSING_FIELDS_DIAGNOSTIC_MESSAGE], - [undefined, { time: [0] }, true, 1, EVENT_ERROR_DIAGNOSTIC_MESSAGE], - [undefined, { time: [0] }, false, 2, EVENT_ERROR_DIAGNOSTIC_MESSAGE], + ['api_key', undefined, true, 2, DIAGNOSTIC_MESSAGES.INVALID_OR_MISSING_FIELDS], + [undefined, { time: [0] }, true, 1, DIAGNOSTIC_MESSAGES.EVENT_ERROR], + [undefined, { time: [0] }, false, 2, DIAGNOSTIC_MESSAGES.EVENT_ERROR], ])( 'should track diagnostic when 400', async (missingField, eventsWithInvalidFields, useRetry, dropCount, message) => { @@ -602,7 +596,7 @@ describe('destination', () => { ); expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); - expect(diagnosticProvider.track).toHaveBeenCalledWith(2, 413, PAYLOAD_TOO_LARGE_DIAGNOSTIC_MESSAGE); + expect(diagnosticProvider.track).toHaveBeenCalledWith(2, 413, DIAGNOSTIC_MESSAGES.PAYLOAD_TOO_LARGE); }); test.each([ @@ -654,7 +648,7 @@ describe('destination', () => { useRetry, ); expect(diagnosticProvider.track).toHaveBeenCalledTimes(1); - expect(diagnosticProvider.track).toHaveBeenCalledWith(dropCount, 429, EXCEEDED_DAILY_QUOTA_DIAGNOSTIC_MESSAGE); + expect(diagnosticProvider.track).toHaveBeenCalledWith(dropCount, 429, DIAGNOSTIC_MESSAGES.EXCEEDED_DAILY_QUOTA); expect(destination.queue.length).toBe(queueCount); }); }); From f72cf0e9ac8b281c2d447b4efafecc2b38ff640c Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Thu, 12 Oct 2023 14:47:10 -0700 Subject: [PATCH 37/50] Update packages/analytics-core/src/config.ts Co-authored-by: Justin Fiedler --- packages/analytics-core/src/config.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/analytics-core/src/config.ts b/packages/analytics-core/src/config.ts index c5c1579bc..d0fb5569c 100644 --- a/packages/analytics-core/src/config.ts +++ b/packages/analytics-core/src/config.ts @@ -80,11 +80,7 @@ export class Config implements IConfig { this.transportProvider = options.transportProvider; this.useBatch = options.useBatch ?? defaultConfig.useBatch; - if (options.diagnosticProvider == undefined) { - this.diagnosticProvider = defaultConfig.diagnosticProvider; - } else { - this.diagnosticProvider = options.diagnosticProvider; - } + this.diagnosticProvider = options.diagnosticProvider ?? defaultConfig.diagnosticProvider; this.diagnosticProvider.apiKey = this.apiKey; this.loggerProvider.enable(this.logLevel); From 3db01cdd73148677e53375138b39aff859c96eb5 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Thu, 12 Oct 2023 14:47:51 -0700 Subject: [PATCH 38/50] Update packages/analytics-core/test/diagnostic.test.ts Co-authored-by: Justin Fiedler --- packages/analytics-core/test/diagnostic.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/analytics-core/test/diagnostic.test.ts b/packages/analytics-core/test/diagnostic.test.ts index b07c2e0f6..85938fa50 100644 --- a/packages/analytics-core/test/diagnostic.test.ts +++ b/packages/analytics-core/test/diagnostic.test.ts @@ -59,7 +59,7 @@ describe('Diagnostic', () => { expect(diagnostic.queue[0].library).toBe('amplitude-ts'); }); - test('should not add to queen when disabled', () => { + test('should not add to queue when disabled', () => { diagnostic.isDisabled = true; diagnostic.track(eventCount, code, 'Test message'); From 158652082945fab2a25f31c148014fe79d830030 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Thu, 12 Oct 2023 14:59:39 -0700 Subject: [PATCH 39/50] fix: change api only when it's not set --- packages/analytics-core/src/config.ts | 4 +++- packages/analytics-core/test/diagnostic.test.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/analytics-core/src/config.ts b/packages/analytics-core/src/config.ts index d0fb5569c..d6ac8d49e 100644 --- a/packages/analytics-core/src/config.ts +++ b/packages/analytics-core/src/config.ts @@ -81,7 +81,9 @@ export class Config implements IConfig { this.useBatch = options.useBatch ?? defaultConfig.useBatch; this.diagnosticProvider = options.diagnosticProvider ?? defaultConfig.diagnosticProvider; - this.diagnosticProvider.apiKey = this.apiKey; + if (!this.diagnosticProvider.apiKey) { + this.diagnosticProvider.apiKey = this.apiKey; + } this.loggerProvider.enable(this.logLevel); diff --git a/packages/analytics-core/test/diagnostic.test.ts b/packages/analytics-core/test/diagnostic.test.ts index 85938fa50..11f517859 100644 --- a/packages/analytics-core/test/diagnostic.test.ts +++ b/packages/analytics-core/test/diagnostic.test.ts @@ -21,6 +21,7 @@ describe('Diagnostic', () => { test('should set default values if not provided', () => { expect(diagnostic.serverUrl).toBe(DIAGNOSTIC_ENDPOINT); expect(diagnostic.isDisabled).toBe(false); + expect(diagnostic.apiKey).toBe(''); }); test('should set isDisabled to provided value', () => { From 11dcde799477ced07ffb1d8feb4db8d617a34202 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Thu, 12 Oct 2023 15:27:32 -0700 Subject: [PATCH 40/50] fix: diagnostic not extend options --- packages/analytics-core/test/helpers/default.ts | 2 -- packages/analytics-types/src/diagnostic.ts | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/analytics-core/test/helpers/default.ts b/packages/analytics-core/test/helpers/default.ts index 5782d940e..a52059ea3 100644 --- a/packages/analytics-core/test/helpers/default.ts +++ b/packages/analytics-core/test/helpers/default.ts @@ -17,8 +17,6 @@ export const useDefaultConfig = (): Config => { }, ...getDefaultConfig(), diagnosticProvider: { - isDisabled: false, - serverUrl: undefined, apiKey: undefined, track: jest.fn(), }, diff --git a/packages/analytics-types/src/diagnostic.ts b/packages/analytics-types/src/diagnostic.ts index 8ad557d67..5b5423775 100644 --- a/packages/analytics-types/src/diagnostic.ts +++ b/packages/analytics-types/src/diagnostic.ts @@ -4,6 +4,7 @@ export interface DiagnosticOptions { apiKey?: string; } -export interface Diagnostic extends DiagnosticOptions { +export interface Diagnostic { + apiKey?: string; track(eventCount: number, code: number, message: string): void; } From eb6d09549e8191671c9ba56f1062c257dd41fa71 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Thu, 12 Oct 2023 15:27:44 -0700 Subject: [PATCH 41/50] fix: disable BaseDiagnostic --- .../src/diagnostics/diagnostic.ts | 2 ++ .../analytics-core/src/diagnostics/diagnostic.ts | 6 +++--- packages/analytics-core/test/diagnostic.test.ts | 16 +++++++++------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/analytics-browser/src/diagnostics/diagnostic.ts b/packages/analytics-browser/src/diagnostics/diagnostic.ts index 223044313..b4afd1881 100644 --- a/packages/analytics-browser/src/diagnostics/diagnostic.ts +++ b/packages/analytics-browser/src/diagnostics/diagnostic.ts @@ -1,6 +1,8 @@ import { BaseDiagnostic } from '@amplitude/analytics-core'; export class BrowserDiagnostic extends BaseDiagnostic { + isDisabled = false; + async flush(): Promise { await fetch(this.serverUrl, this.requestPayloadBuilder(this.queue)); await super.flush(); diff --git a/packages/analytics-core/src/diagnostics/diagnostic.ts b/packages/analytics-core/src/diagnostics/diagnostic.ts index 724b6c8db..ab613e120 100644 --- a/packages/analytics-core/src/diagnostics/diagnostic.ts +++ b/packages/analytics-core/src/diagnostics/diagnostic.ts @@ -4,9 +4,9 @@ import { DiagnosticOmniMetrics } from './typings'; import { DIAGNOSTIC_METADATA_TYPE } from './constants'; export class BaseDiagnostic implements Diagnostic { - isDisabled = false; + isDisabled = true; serverUrl: string = DIAGNOSTIC_ENDPOINT; - apiKey?: string = ''; + apiKey = ''; queue: DiagnosticOmniMetrics[] = []; private scheduled: ReturnType | null = null; @@ -15,7 +15,7 @@ export class BaseDiagnostic implements Diagnostic { private delay = 60000; constructor(options?: DiagnosticOptions) { - this.isDisabled = options && options.isDisabled ? options.isDisabled : false; + this.isDisabled = options && options.isDisabled != undefined ? options.isDisabled : true; this.serverUrl = options && options.serverUrl ? options.serverUrl : DIAGNOSTIC_ENDPOINT; this.apiKey = options && options.apiKey ? options.apiKey : ''; } diff --git a/packages/analytics-core/test/diagnostic.test.ts b/packages/analytics-core/test/diagnostic.test.ts index 11f517859..399d60ccb 100644 --- a/packages/analytics-core/test/diagnostic.test.ts +++ b/packages/analytics-core/test/diagnostic.test.ts @@ -20,15 +20,15 @@ describe('Diagnostic', () => { describe('constructor', () => { test('should set default values if not provided', () => { expect(diagnostic.serverUrl).toBe(DIAGNOSTIC_ENDPOINT); - expect(diagnostic.isDisabled).toBe(false); + expect(diagnostic.isDisabled).toBe(true); expect(diagnostic.apiKey).toBe(''); }); test('should set isDisabled to provided value', () => { - const isDisabled = true; + const isDisabled = false; diagnostic = new BaseDiagnostic({ isDisabled }); expect(diagnostic.serverUrl).toBe(DIAGNOSTIC_ENDPOINT); - expect(diagnostic.isDisabled).toBe(isDisabled); + expect(diagnostic.isDisabled).toBe(false); expect(diagnostic.apiKey).toBe(''); }); @@ -36,7 +36,7 @@ describe('Diagnostic', () => { const serverUrl = 'https://test.com'; diagnostic = new BaseDiagnostic({ serverUrl }); expect(diagnostic.serverUrl).toBe(serverUrl); - expect(diagnostic.isDisabled).toBe(false); + expect(diagnostic.isDisabled).toBe(true); expect(diagnostic.apiKey).toBe(''); }); @@ -44,13 +44,14 @@ describe('Diagnostic', () => { const apiKey = '12345'; diagnostic = new BaseDiagnostic({ apiKey }); expect(diagnostic.serverUrl).toBe(DIAGNOSTIC_ENDPOINT); - expect(diagnostic.isDisabled).toBe(false); + expect(diagnostic.isDisabled).toBe(true); expect(diagnostic.apiKey).toBe('12345'); }); }); describe('track', () => { test('should add events to the queue when track method is called', () => { + diagnostic.isDisabled = false; diagnostic.track(eventCount, code, 'Test message'); expect(diagnostic.queue).toHaveLength(1); @@ -60,8 +61,7 @@ describe('Diagnostic', () => { expect(diagnostic.queue[0].library).toBe('amplitude-ts'); }); - test('should not add to queue when disabled', () => { - diagnostic.isDisabled = true; + test('should not add to queue by default', () => { diagnostic.track(eventCount, code, 'Test message'); expect(diagnostic.queue).toHaveLength(0); @@ -70,6 +70,7 @@ describe('Diagnostic', () => { test('should schedule flush when track is called for the first time', () => { const setTimeoutMock = jest.spyOn(global, 'setTimeout'); + diagnostic.isDisabled = false; diagnostic.track(eventCount, code, 'Test message'); jest.advanceTimersByTime(delay); @@ -85,6 +86,7 @@ describe('Diagnostic', () => { const clearTimeoutMock = jest.spyOn(global, 'clearTimeout'); const setTimeoutMock = jest.spyOn(global, 'setTimeout'); + diagnostic.isDisabled = false; diagnostic.track(eventCount, code, 'Scheduled timeout test'); await diagnostic.flush(); From e73316960dca0f12d0b38c385ca3ae40adf55759 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Fri, 13 Oct 2023 10:22:51 -0700 Subject: [PATCH 42/50] fix: recover fulfillRequest --- .../analytics-core/src/plugins/destination.ts | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index ebca62d9f..7db024fe2 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -160,6 +160,13 @@ export class Destination implements DestinationPlugin { this.fulfillRequest(list, 0, UNEXPECTED_ERROR_MESSAGE); return; } + if (!useRetry) { + if ('body' in res) { + this.fulfillRequest(list, res.statusCode, `${res.status}: ${getResponseBodyString(res)}`); + } else { + this.fulfillRequest(list, res.statusCode, res.status); + } + } this.handleResponse(res, list, useRetry); } catch (e) { const errorMessage = getErrorMessage(e); @@ -174,7 +181,7 @@ export class Destination implements DestinationPlugin { switch (status) { case Status.Success: { - this.handleSuccessResponse(res, list); + this.handleSuccessResponse(res, list, useRetry); break; } case Status.Invalid: { @@ -199,14 +206,18 @@ export class Destination implements DestinationPlugin { } } - handleSuccessResponse(res: SuccessResponse, list: Context[]) { - this.fulfillRequest(list, res.statusCode, SUCCESS_MESSAGE); + handleSuccessResponse(res: SuccessResponse, list: Context[], useRetry: boolean) { + if (useRetry) { + this.fulfillRequest(list, res.statusCode, SUCCESS_MESSAGE); + } } handleInvalidResponse(res: InvalidResponse, list: Context[], useRetry: boolean) { if (res.body.missingField || res.body.error.startsWith(INVALID_API_KEY)) { - this.fulfillRequest(list, res.statusCode, `${res.status}: ${getResponseBodyString(res)}`); this.config.diagnosticProvider.track(list.length, 400, DIAGNOSTIC_MESSAGES.INVALID_OR_MISSING_FIELDS); + if (useRetry) { + this.fulfillRequest(list, res.statusCode, `${res.status}: ${getResponseBodyString(res)}`); + } return; } @@ -241,7 +252,12 @@ export class Destination implements DestinationPlugin { } handlePayloadTooLargeResponse(res: PayloadTooLargeResponse, list: Context[], useRetry: boolean) { - if (list.length === 1 || !useRetry) { + if (!useRetry) { + this.config.diagnosticProvider.track(list.length, 413, DIAGNOSTIC_MESSAGES.PAYLOAD_TOO_LARGE); + return; + } + + if (list.length === 1) { this.config.diagnosticProvider.track(list.length, 413, DIAGNOSTIC_MESSAGES.PAYLOAD_TOO_LARGE); this.fulfillRequest(list, res.statusCode, res.body.error); return; @@ -257,7 +273,6 @@ export class Destination implements DestinationPlugin { handleRateLimitResponse(res: RateLimitResponse, list: Context[], useRetry: boolean) { if (!useRetry) { this.config.diagnosticProvider.track(list.length, 429, DIAGNOSTIC_MESSAGES.EXCEEDED_DAILY_QUOTA); - this.fulfillRequest(list, res.statusCode, res.status); return; } @@ -304,7 +319,6 @@ export class Destination implements DestinationPlugin { }), ); } else { - this.fulfillRequest(list, res.statusCode, res.status); this.config.diagnosticProvider.track(list.length, 0, DIAGNOSTIC_MESSAGES.UNEXPECTED_ERROR); } } From 85cf273f7c3f6e624d768dddfe0af63ddb974326 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Tue, 17 Oct 2023 13:30:01 -0700 Subject: [PATCH 43/50] fix: should set default api key for diagnostic --- packages/analytics-browser/src/config.ts | 2 +- packages/analytics-browser/test/browser-client.test.ts | 5 +++++ packages/analytics-browser/test/config.test.ts | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/analytics-browser/src/config.ts b/packages/analytics-browser/src/config.ts index 58bb9ce8e..30885ec8d 100644 --- a/packages/analytics-browser/src/config.ts +++ b/packages/analytics-browser/src/config.ts @@ -80,7 +80,7 @@ export class BrowserConfig extends Config implements IBrowserConfig { public diagnosticProvider: Diagnostic = new BrowserDiagnostic(), userId?: string, ) { - super({ apiKey, storageProvider, transportProvider: createTransport(transport) }); + super({ apiKey, storageProvider, transportProvider: createTransport(transport), diagnosticProvider }); this._cookieStorage = cookieStorage; this.deviceId = deviceId; this.lastEventId = lastEventId; diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index 2dc7c94f9..47072f795 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -263,6 +263,11 @@ describe('browser-client', () => { }); describe('diagnostic', () => { + test('should init diagnostic with default api key', async () => { + await client.init(apiKey).promise; + expect(client.config.diagnosticProvider.apiKey).toBe(apiKey); + }); + test('should not diagnostic track when 200', async () => { const transportProvider = { send: jest.fn().mockImplementationOnce(() => { diff --git a/packages/analytics-browser/test/config.test.ts b/packages/analytics-browser/test/config.test.ts index 3935c45b3..c7338662d 100644 --- a/packages/analytics-browser/test/config.test.ts +++ b/packages/analytics-browser/test/config.test.ts @@ -82,6 +82,11 @@ describe('config', () => { const config = new Config.BrowserConfig(apiKey); expect(config.optOut).toBe(false); }); + + test('should set default api key for diagnostic provider', () => { + const config = new Config.BrowserConfig(apiKey); + expect(config.diagnosticProvider.apiKey).toBe(apiKey); + }); }); describe('useBrowserConfig', () => { From 546b5b0e22c27835707ae6fa0301c23be41d0ab8 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Tue, 17 Oct 2023 13:35:11 -0700 Subject: [PATCH 44/50] fix: time should be in minutes --- packages/analytics-core/src/diagnostics/diagnostic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/analytics-core/src/diagnostics/diagnostic.ts b/packages/analytics-core/src/diagnostics/diagnostic.ts index ab613e120..733e51cc6 100644 --- a/packages/analytics-core/src/diagnostics/diagnostic.ts +++ b/packages/analytics-core/src/diagnostics/diagnostic.ts @@ -48,7 +48,7 @@ export class BaseDiagnostic implements Diagnostic { return { metadata_type: DIAGNOSTIC_METADATA_TYPE, library: 'amplitude-ts', - accounting_time_min: Date.now(), + accounting_time_min: Math.floor(Date.now() / 60 / 1000), response_code: code, trigger: message, action: 'drop events', From 9dfbe7a8ddcc1a055779cf99ab8acce91853abad Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Tue, 17 Oct 2023 14:32:21 -0700 Subject: [PATCH 45/50] fix: should diagnostic track only when api key is valid --- .../test/browser-client.test.ts | 22 +++++++++++++++++++ .../analytics-core/src/plugins/destination.ts | 5 +++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/analytics-browser/test/browser-client.test.ts b/packages/analytics-browser/test/browser-client.test.ts index 47072f795..f28671382 100644 --- a/packages/analytics-browser/test/browser-client.test.ts +++ b/packages/analytics-browser/test/browser-client.test.ts @@ -306,6 +306,28 @@ describe('browser-client', () => { expect(diagnosticTrack).toHaveBeenCalledWith(1, 0, core.DIAGNOSTIC_MESSAGES.UNEXPECTED_ERROR); }); + test('should diagnostic track when flush and non 200', async () => { + const transportProvider = { + send: jest.fn().mockImplementationOnce(() => { + return Promise.resolve({ + status: Status.Failed, + statusCode: 500, + }); + }), + }; + + await client.init(apiKey, { + defaultTracking: false, + }).promise; + const diagnosticTrack = jest.spyOn(client.config.diagnosticProvider, 'track'); + client.config.transportProvider = transportProvider; + client.track('event_type', { userId: 'user_0' }); + await client.flush().promise; + + expect(diagnosticTrack).toHaveBeenCalledTimes(1); + expect(diagnosticTrack).toHaveBeenCalledWith(1, 0, core.DIAGNOSTIC_MESSAGES.UNEXPECTED_ERROR); + }); + test.each([ ['api_key', undefined, core.DIAGNOSTIC_MESSAGES.INVALID_OR_MISSING_FIELDS], [undefined, { time: [0] }, core.DIAGNOSTIC_MESSAGES.EVENT_ERROR], diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index 7db024fe2..07a77a71f 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -135,7 +135,6 @@ export class Destination implements DestinationPlugin { async send(list: Context[], useRetry = true) { if (!this.config.apiKey) { - this.config.diagnosticProvider.track(list.length, 400, DIAGNOSTIC_MESSAGES.MISSING_API_KEY); return this.fulfillRequest(list, 400, MISSING_API_KEY_MESSAGE); } @@ -214,7 +213,9 @@ export class Destination implements DestinationPlugin { handleInvalidResponse(res: InvalidResponse, list: Context[], useRetry: boolean) { if (res.body.missingField || res.body.error.startsWith(INVALID_API_KEY)) { - this.config.diagnosticProvider.track(list.length, 400, DIAGNOSTIC_MESSAGES.INVALID_OR_MISSING_FIELDS); + if (!res.body.error.startsWith(INVALID_API_KEY)) { + this.config.diagnosticProvider.track(list.length, 400, DIAGNOSTIC_MESSAGES.INVALID_OR_MISSING_FIELDS); + } if (useRetry) { this.fulfillRequest(list, res.statusCode, `${res.status}: ${getResponseBodyString(res)}`); } From 2e373dc25260d419392ed9ae37307f5c4a3fb505 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Tue, 17 Oct 2023 16:02:11 -0700 Subject: [PATCH 46/50] test: apply recent changes 1. account time in minutes 2. browser diagnostic default apiKey --- packages/analytics-browser/test/config.test.ts | 6 +++--- packages/analytics-browser/test/diagnostic.test.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/analytics-browser/test/config.test.ts b/packages/analytics-browser/test/config.test.ts index c7338662d..7f6f157c6 100644 --- a/packages/analytics-browser/test/config.test.ts +++ b/packages/analytics-browser/test/config.test.ts @@ -74,7 +74,7 @@ describe('config', () => { transport: 'fetch', transportProvider: new FetchTransport(), useBatch: false, - diagnosticProvider: new BrowserDiagnostic(), + diagnosticProvider: new BrowserDiagnostic({ apiKey }), }); }); @@ -135,7 +135,7 @@ describe('config', () => { transport: 'fetch', transportProvider: new FetchTransport(), useBatch: false, - diagnosticProvider: new BrowserDiagnostic(), + diagnosticProvider: new BrowserDiagnostic({ apiKey }), }); expect(getTopLevelDomain).toHaveBeenCalledTimes(1); }); @@ -221,7 +221,7 @@ describe('config', () => { transport: 'fetch', transportProvider: new FetchTransport(), useBatch: false, - diagnosticProvider: new BrowserDiagnostic(), + diagnosticProvider: new BrowserDiagnostic({ apiKey }), }); }); diff --git a/packages/analytics-browser/test/diagnostic.test.ts b/packages/analytics-browser/test/diagnostic.test.ts index dac75c017..7a73a13fd 100644 --- a/packages/analytics-browser/test/diagnostic.test.ts +++ b/packages/analytics-browser/test/diagnostic.test.ts @@ -5,7 +5,7 @@ describe('Diagnostic', () => { test('should fetch', async () => { const diagnostic = new BrowserDiagnostic(); const fetchMock = jest.fn().mockResolvedValueOnce({} as Response); - Date.now = jest.fn().mockReturnValue(12345); + Date.now = jest.fn().mockReturnValue(1697583342266); global.fetch = fetchMock; diagnostic.track(5, 400, 'test message 0'); @@ -24,7 +24,7 @@ describe('Diagnostic', () => { { metadata_type: DIAGNOSTIC_METADATA_TYPE, library: 'amplitude-ts', - accounting_time_min: 12345, + accounting_time_min: Math.floor(1697583342266 / 60 / 1000), response_code: 400, trigger: 'test message 0', action: 'drop events', @@ -33,7 +33,7 @@ describe('Diagnostic', () => { { metadata_type: DIAGNOSTIC_METADATA_TYPE, library: 'amplitude-ts', - accounting_time_min: 12345, + accounting_time_min: Math.floor(1697583342266 / 60 / 1000), response_code: 500, trigger: 'test message 1', action: 'drop events', From a6c34f542c93d8e10d10d4655d53d44c6e196b00 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 8 Nov 2023 13:51:23 -0800 Subject: [PATCH 47/50] chore: update dropList with retryList --- packages/analytics-core/src/plugins/destination.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index 07a77a71f..d2cfd6472 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -283,6 +283,7 @@ export class Destination implements DestinationPlugin { const dropUserIdsSet = new Set(dropUserIds); const dropDeviceIdsSet = new Set(dropDeviceIds); const throttledIndexSet = new Set(throttledIndex); + const dropList = []; const retry = list.filter((context, index) => { if ( @@ -290,6 +291,7 @@ export class Destination implements DestinationPlugin { (context.event.device_id && dropDeviceIdsSet.has(context.event.device_id)) ) { this.fulfillRequest([context], res.statusCode, res.body.error); + dropList.push(context); return; } if (throttledIndexSet.has(index)) { @@ -298,9 +300,8 @@ export class Destination implements DestinationPlugin { return true; }); - const dropEvents = retry.filter((element) => !retry.includes(element)); - if (dropEvents.length > 0) { - this.config.diagnosticProvider.track(dropEvents.length, 429, DIAGNOSTIC_MESSAGES.EXCEEDED_DAILY_QUOTA); + if (dropList.length > 0) { + this.config.diagnosticProvider.track(dropList.length, 429, DIAGNOSTIC_MESSAGES.EXCEEDED_DAILY_QUOTA); } if (retry.length > 0) { From d960c1856cb3f8a4d2e888735874758f7331933a Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 8 Nov 2023 11:59:21 -0800 Subject: [PATCH 48/50] chore: use nullish coalescing Co-authored-by: Justin Fiedler --- packages/analytics-core/src/diagnostics/diagnostic.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/analytics-core/src/diagnostics/diagnostic.ts b/packages/analytics-core/src/diagnostics/diagnostic.ts index 733e51cc6..1e62b7b6e 100644 --- a/packages/analytics-core/src/diagnostics/diagnostic.ts +++ b/packages/analytics-core/src/diagnostics/diagnostic.ts @@ -15,9 +15,9 @@ export class BaseDiagnostic implements Diagnostic { private delay = 60000; constructor(options?: DiagnosticOptions) { - this.isDisabled = options && options.isDisabled != undefined ? options.isDisabled : true; - this.serverUrl = options && options.serverUrl ? options.serverUrl : DIAGNOSTIC_ENDPOINT; - this.apiKey = options && options.apiKey ? options.apiKey : ''; + this.isDisabled = options?.isDisabled ?? true; + this.serverUrl = options?.serverUrl ?? DIAGNOSTIC_ENDPOINT; + this.apiKey = options?.apiKey ?? ''; } track(eventCount: number, code: number, message: string) { From d2bbc02bd51ce8998869b571cdc1c98eac73f0bb Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 8 Nov 2023 14:19:14 -0800 Subject: [PATCH 49/50] refactor: extract track diagnostic when not useRetry --- .../analytics-core/src/plugins/destination.ts | 124 ++++++++++-------- 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/packages/analytics-core/src/plugins/destination.ts b/packages/analytics-core/src/plugins/destination.ts index d2cfd6472..2c5902994 100644 --- a/packages/analytics-core/src/plugins/destination.ts +++ b/packages/analytics-core/src/plugins/destination.ts @@ -165,8 +165,10 @@ export class Destination implements DestinationPlugin { } else { this.fulfillRequest(list, res.statusCode, res.status); } + this.processResponseDiagnostics(res, list); + return; } - this.handleResponse(res, list, useRetry); + this.handleResponse(res, list); } catch (e) { const errorMessage = getErrorMessage(e); this.config.loggerProvider.error(errorMessage); @@ -175,50 +177,74 @@ export class Destination implements DestinationPlugin { } } - handleResponse(res: Response, list: Context[], useRetry: boolean) { + processResponseDiagnostics(res: Response, list: Context[]) { + const { status } = res; + + switch (status) { + case Status.Invalid: { + if (res.body.missingField && !res.body.error.startsWith(INVALID_API_KEY)) { + this.config.diagnosticProvider.track(list.length, 400, DIAGNOSTIC_MESSAGES.INVALID_OR_MISSING_FIELDS); + } else { + this.config.diagnosticProvider.track(list.length, 400, DIAGNOSTIC_MESSAGES.EVENT_ERROR); + } + break; + } + case Status.PayloadTooLarge: { + this.config.diagnosticProvider.track(list.length, 413, DIAGNOSTIC_MESSAGES.PAYLOAD_TOO_LARGE); + break; + } + case Status.RateLimit: { + this.config.diagnosticProvider.track(list.length, 429, DIAGNOSTIC_MESSAGES.EXCEEDED_DAILY_QUOTA); + break; + } + default: { + this.config.diagnosticProvider.track(list.length, 0, DIAGNOSTIC_MESSAGES.UNEXPECTED_ERROR); + break; + } + } + } + + handleResponse(res: Response, list: Context[]) { const { status } = res; switch (status) { case Status.Success: { - this.handleSuccessResponse(res, list, useRetry); + this.handleSuccessResponse(res, list); break; } case Status.Invalid: { - this.handleInvalidResponse(res, list, useRetry); + this.handleInvalidResponse(res, list); break; } case Status.PayloadTooLarge: { - this.handlePayloadTooLargeResponse(res, list, useRetry); + this.handlePayloadTooLargeResponse(res, list); break; } case Status.RateLimit: { - this.handleRateLimitResponse(res, list, useRetry); + this.handleRateLimitResponse(res, list); break; } default: { // log intermediate event status before retry this.config.loggerProvider.warn(`{code: 0, error: "Status '${status}' provided for ${list.length} events"}`); - this.handleOtherResponse(list, useRetry); + this.handleOtherResponse(list); break; } } } - handleSuccessResponse(res: SuccessResponse, list: Context[], useRetry: boolean) { - if (useRetry) { - this.fulfillRequest(list, res.statusCode, SUCCESS_MESSAGE); - } + handleSuccessResponse(res: SuccessResponse, list: Context[]) { + this.fulfillRequest(list, res.statusCode, SUCCESS_MESSAGE); } - handleInvalidResponse(res: InvalidResponse, list: Context[], useRetry: boolean) { + handleInvalidResponse(res: InvalidResponse, list: Context[]) { + if (res.body.missingField && !res.body.error.startsWith(INVALID_API_KEY)) { + this.config.diagnosticProvider.track(list.length, 400, DIAGNOSTIC_MESSAGES.INVALID_OR_MISSING_FIELDS); + } + if (res.body.missingField || res.body.error.startsWith(INVALID_API_KEY)) { - if (!res.body.error.startsWith(INVALID_API_KEY)) { - this.config.diagnosticProvider.track(list.length, 400, DIAGNOSTIC_MESSAGES.INVALID_OR_MISSING_FIELDS); - } - if (useRetry) { - this.fulfillRequest(list, res.statusCode, `${res.status}: ${getResponseBodyString(res)}`); - } + this.fulfillRequest(list, res.statusCode, res.body.error); return; } @@ -230,34 +256,25 @@ export class Destination implements DestinationPlugin { ].flat(); const dropIndexSet = new Set(dropIndex); - if (useRetry) { - if (dropIndexSet.size) { - this.config.diagnosticProvider.track(dropIndexSet.size, 400, DIAGNOSTIC_MESSAGES.EVENT_ERROR); + if (dropIndexSet.size) { + this.config.diagnosticProvider.track(dropIndexSet.size, 400, DIAGNOSTIC_MESSAGES.EVENT_ERROR); + } + const retry = list.filter((context, index) => { + if (dropIndexSet.has(index)) { + this.fulfillRequest([context], res.statusCode, res.body.error); + return; } - const retry = list.filter((context, index) => { - if (dropIndexSet.has(index)) { - this.fulfillRequest([context], res.statusCode, res.body.error); - return; - } - return true; - }); + return true; + }); - if (retry.length > 0) { - // log intermediate event status before retry - this.config.loggerProvider.warn(getResponseBodyString(res)); - } - this.addToQueue(...retry); - } else { - this.config.diagnosticProvider.track(list.length, 400, DIAGNOSTIC_MESSAGES.EVENT_ERROR); + if (retry.length > 0) { + // log intermediate event status before retry + this.config.loggerProvider.warn(getResponseBodyString(res)); } + this.addToQueue(...retry); } - handlePayloadTooLargeResponse(res: PayloadTooLargeResponse, list: Context[], useRetry: boolean) { - if (!useRetry) { - this.config.diagnosticProvider.track(list.length, 413, DIAGNOSTIC_MESSAGES.PAYLOAD_TOO_LARGE); - return; - } - + handlePayloadTooLargeResponse(res: PayloadTooLargeResponse, list: Context[]) { if (list.length === 1) { this.config.diagnosticProvider.track(list.length, 413, DIAGNOSTIC_MESSAGES.PAYLOAD_TOO_LARGE); this.fulfillRequest(list, res.statusCode, res.body.error); @@ -271,12 +288,7 @@ export class Destination implements DestinationPlugin { this.addToQueue(...list); } - handleRateLimitResponse(res: RateLimitResponse, list: Context[], useRetry: boolean) { - if (!useRetry) { - this.config.diagnosticProvider.track(list.length, 429, DIAGNOSTIC_MESSAGES.EXCEEDED_DAILY_QUOTA); - return; - } - + handleRateLimitResponse(res: RateLimitResponse, list: Context[]) { const dropUserIds = Object.keys(res.body.exceededDailyQuotaUsers); const dropDeviceIds = Object.keys(res.body.exceededDailyQuotaDevices); const throttledIndex = res.body.throttledEvents; @@ -312,17 +324,13 @@ export class Destination implements DestinationPlugin { this.addToQueue(...retry); } - handleOtherResponse(list: Context[], useRetry: boolean) { - if (useRetry) { - this.addToQueue( - ...list.map((context) => { - context.timeout = context.attempts * this.retryTimeout; - return context; - }), - ); - } else { - this.config.diagnosticProvider.track(list.length, 0, DIAGNOSTIC_MESSAGES.UNEXPECTED_ERROR); - } + handleOtherResponse(list: Context[]) { + this.addToQueue( + ...list.map((context) => { + context.timeout = context.attempts * this.retryTimeout; + return context; + }), + ); } fulfillRequest(list: Context[], code: number, message: string) { From 62aaa6f7715b2f084474ad5834bc41423cae338e Mon Sep 17 00:00:00 2001 From: amplitude-sdk-bot Date: Thu, 16 Nov 2023 18:08:10 +0000 Subject: [PATCH 50/50] chore(release): publish - @amplitude/analytics-browser@2.4.0-featadddiagnostics.0 - @amplitude/analytics-client-common@2.0.8-featadddiagnostics.0 - @amplitude/analytics-core@2.2.0-featadddiagnostics.0 - @amplitude/analytics-types@2.4.0-featadddiagnostics.0 - @amplitude/plugin-global-user-properties@1.2.2-featadddiagnostics.0 - @amplitude/plugin-page-view-tracking-browser@2.0.14-featadddiagnostics.0 - @amplitude/plugin-web-attribution-browser@2.0.14-featadddiagnostics.0 --- packages/analytics-browser/CHANGELOG.md | 32 +++++++++++ packages/analytics-browser/README.md | 2 +- .../amplitude-bookmarklet-snippet.js | 4 +- .../generated/amplitude-gtm-snippet.js | 4 +- .../generated/amplitude-snippet.js | 4 +- packages/analytics-browser/package.json | 12 ++-- .../analytics-browser/playground/amplitude.js | 2 +- packages/analytics-browser/src/version.ts | 2 +- packages/analytics-client-common/CHANGELOG.md | 9 +++ packages/analytics-client-common/package.json | 6 +- packages/analytics-core/CHANGELOG.md | 56 +++++++++++++++++++ packages/analytics-core/package.json | 4 +- packages/analytics-types/CHANGELOG.md | 29 ++++++++++ packages/analytics-types/package.json | 2 +- .../CHANGELOG.md | 9 +++ .../package.json | 6 +- .../CHANGELOG.md | 9 +++ .../package.json | 8 +-- .../CHANGELOG.md | 9 +++ .../package.json | 10 ++-- 20 files changed, 186 insertions(+), 33 deletions(-) diff --git a/packages/analytics-browser/CHANGELOG.md b/packages/analytics-browser/CHANGELOG.md index 2e60b8f17..dbac6d366 100644 --- a/packages/analytics-browser/CHANGELOG.md +++ b/packages/analytics-browser/CHANGELOG.md @@ -3,6 +3,38 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.4.0-featadddiagnostics.0](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@2.3.3...@amplitude/analytics-browser@2.4.0-featadddiagnostics.0) (2023-11-16) + +### Bug Fixes + +- diagnosticProvider should be in core config + ([c0ba911](https://github.com/amplitude/Amplitude-TypeScript/commit/c0ba9114944febccb1592dd457453659580fe3eb)) +- disable BaseDiagnostic + ([eb6d095](https://github.com/amplitude/Amplitude-TypeScript/commit/eb6d09549e8191671c9ba56f1062c257dd41fa71)) +- fetch ([957ec26](https://github.com/amplitude/Amplitude-TypeScript/commit/957ec26f45d9ae71cf45f7f79e1c3ef855c6b584)) +- init Destination with config + ([cab2ecd](https://github.com/amplitude/Amplitude-TypeScript/commit/cab2ecd19c63dcbee1083b9b883f2bce2ed23588)) +- payload and test + ([d6bac4c](https://github.com/amplitude/Amplitude-TypeScript/commit/d6bac4c63a04d480da960893134e45548f58f0a7)) +- should diagnostic track only when api key is valid + ([9dfbe7a](https://github.com/amplitude/Amplitude-TypeScript/commit/9dfbe7a8ddcc1a055779cf99ab8acce91853abad)) +- should set default api key for diagnostic + ([85cf273](https://github.com/amplitude/Amplitude-TypeScript/commit/85cf273f7c3f6e624d768dddfe0af63ddb974326)) + +### Features + +- diagnostic options + ([d927a22](https://github.com/amplitude/Amplitude-TypeScript/commit/d927a2280feb49cbdcf1ffd2233c2f3b034eb7d7)) +- diagnostic provider only accepts Diagnostic + ([6cb202b](https://github.com/amplitude/Amplitude-TypeScript/commit/6cb202b93a7907a1b09143858bdeb68cc25ab73e)) +- init with diagnostciProvider + ([86bd79d](https://github.com/amplitude/Amplitude-TypeScript/commit/86bd79d4333a5dc4a23e84176062e802be259407)) + +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + ## [2.3.3](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@2.3.2...@amplitude/analytics-browser@2.3.3) (2023-10-18) **Note:** Version bump only for package @amplitude/analytics-browser diff --git a/packages/analytics-browser/README.md b/packages/analytics-browser/README.md index 28ad99ce5..0805949cd 100644 --- a/packages/analytics-browser/README.md +++ b/packages/analytics-browser/README.md @@ -51,7 +51,7 @@ This SDK is also available through CDN. Copy the script loader below and paste b ```html diff --git a/packages/analytics-browser/generated/amplitude-bookmarklet-snippet.js b/packages/analytics-browser/generated/amplitude-bookmarklet-snippet.js index 410520b4a..74260f996 100644 --- a/packages/analytics-browser/generated/amplitude-bookmarklet-snippet.js +++ b/packages/analytics-browser/generated/amplitude-bookmarklet-snippet.js @@ -51,10 +51,10 @@ s.parentNode.insertBefore(autoTrackingPluginScript, s); var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-sBFQqoO3bx3qM8f+iksNzu/E3v0rdkVlvzRqpvP+ynsUk/uaCIsa+NPjw5N04mpg'; + as.integrity = 'sha384-hjSZzUb8olAZ7faRpMe90KnUkbC4lLTg2WpWk9btPQK3EKAECi000DK9V70xluCB'; as.crossOrigin = 'anonymous'; as.async = false; - as.src = 'https://cdn.amplitude.com/libs/analytics-browser-2.3.3-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/analytics-browser-2.4.0-featadddiagnostics.0-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/analytics-browser/generated/amplitude-gtm-snippet.js b/packages/analytics-browser/generated/amplitude-gtm-snippet.js index d6eb2cad3..ed0af4bac 100644 --- a/packages/analytics-browser/generated/amplitude-gtm-snippet.js +++ b/packages/analytics-browser/generated/amplitude-gtm-snippet.js @@ -55,10 +55,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-DFcRnoab5IXR/Ac/qIdBul+ZZjUoS9JrzZNU8oMvs/43Y/zWX9TG4xny9oRqXTHf'; + as.integrity = 'sha384-acxSy7ucE+IE8JfkXzurweaOuoWHXtEn1LAGdzDRIhm0zBMPI8ctyi0c/vJyj1Ud'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/analytics-browser-gtm-2.3.3-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/analytics-browser-gtm-2.4.0-featadddiagnostics.0-min.js.gz'; as.onload = function () { if (!window.amplitudeGTM.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/analytics-browser/generated/amplitude-snippet.js b/packages/analytics-browser/generated/amplitude-snippet.js index 38d38f819..da5fead2d 100644 --- a/packages/analytics-browser/generated/amplitude-snippet.js +++ b/packages/analytics-browser/generated/amplitude-snippet.js @@ -55,10 +55,10 @@ amplitude.invoked = true; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-sBFQqoO3bx3qM8f+iksNzu/E3v0rdkVlvzRqpvP+ynsUk/uaCIsa+NPjw5N04mpg'; + as.integrity = 'sha384-hjSZzUb8olAZ7faRpMe90KnUkbC4lLTg2WpWk9btPQK3EKAECi000DK9V70xluCB'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/analytics-browser-2.3.3-min.js.gz'; + as.src = 'https://cdn.amplitude.com/libs/analytics-browser-2.4.0-featadddiagnostics.0-min.js.gz'; as.onload = function () { if (!window.amplitude.runQueuedFunctions) { console.log('[Amplitude] Error: could not load SDK'); diff --git a/packages/analytics-browser/package.json b/packages/analytics-browser/package.json index 4e20f088b..af73d6734 100644 --- a/packages/analytics-browser/package.json +++ b/packages/analytics-browser/package.json @@ -1,6 +1,6 @@ { "name": "@amplitude/analytics-browser", - "version": "2.3.3", + "version": "2.4.0-featadddiagnostics.0", "description": "Official Amplitude SDK for Web", "keywords": [ "analytics", @@ -44,11 +44,11 @@ "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" }, "dependencies": { - "@amplitude/analytics-client-common": "^2.0.7", - "@amplitude/analytics-core": "^2.1.0", - "@amplitude/analytics-types": "^2.3.0", - "@amplitude/plugin-page-view-tracking-browser": "^2.0.13", - "@amplitude/plugin-web-attribution-browser": "^2.0.13", + "@amplitude/analytics-client-common": "^2.0.8-featadddiagnostics.0", + "@amplitude/analytics-core": "^2.2.0-featadddiagnostics.0", + "@amplitude/analytics-types": "^2.4.0-featadddiagnostics.0", + "@amplitude/plugin-page-view-tracking-browser": "^2.0.14-featadddiagnostics.0", + "@amplitude/plugin-web-attribution-browser": "^2.0.14-featadddiagnostics.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/analytics-browser/playground/amplitude.js b/packages/analytics-browser/playground/amplitude.js index a39140fad..1e9ee36c3 100644 --- a/packages/analytics-browser/playground/amplitude.js +++ b/packages/analytics-browser/playground/amplitude.js @@ -1 +1 @@ -!function(){"use strict";var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},e(t,n)};function t(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function i(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(i.prototype=n.prototype,new i)}var n=function(){return n=Object.assign||function(e){for(var t,n=1,i=arguments.length;n0&&r[r.length-1])||6!==u[0]&&2!==u[0])){s=0;continue}if(3===u[0]&&(!r||u[1]>r[0]&&u[1]=e.length&&(e=void 0),{value:e&&e[i++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function u(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var i,r,o=n.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(i=o.next()).done;)s.push(i.value)}catch(e){r={error:e}}finally{try{i&&!i.done&&(n=o.return)&&n.call(o)}finally{if(r)throw r.error}}return s}function a(e,t,n){if(n||2===arguments.length)for(var i,r=0,o=t.length;r>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,N)},L=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){var n,i,s;return r(this,void 0,void 0,(function(){return o(this,(function(r){switch(r.label){case 0:return e.name=null!==(n=e.name)&&void 0!==n?n:N(),e.type=null!==(i=e.type)&&void 0!==i?i:"enrichment",[4,null===(s=e.setup)||void 0===s?void 0:s.call(e,t,this.client)];case 1:return r.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){var t;return r(this,void 0,void 0,(function(){var n,i;return o(this,(function(r){switch(r.label){case 0:return n=this.plugins.findIndex((function(t){return t.name===e})),i=this.plugins[n],this.plugins.splice(n,1),[4,null===(t=i.teardown)||void 0===t?void 0:t.call(i)];case 1:return r.sent(),[2]}}))}))},e.prototype.reset=function(e){this.applying=!1,this.plugins.map((function(e){var t;return null===(t=e.teardown)||void 0===t?void 0:t.call(e)})),this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(n){t.queue.push([e,n]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,i,r,a,c,l,d,f,p,v,h,g,y,m,b,_,I,w,S,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=u(e,1),i=t[0],r=u(e,2),a=r[1],c=this.plugins.filter((function(e){return"before"===e.type})),o.label=1;case 1:o.trys.push([1,6,7,8]),l=s(c),d=l.next(),o.label=2;case 2:return d.done?[3,5]:(g=d.value).execute?[4,g.execute(n({},i))]:[3,4];case 3:if(null===(y=o.sent()))return a({event:i,code:0,message:""}),[2];i=y,o.label=4;case 4:return d=l.next(),[3,2];case 5:return[3,8];case 6:return f=o.sent(),I={error:f},[3,8];case 7:try{d&&!d.done&&(w=l.return)&&w.call(l)}finally{if(I)throw I.error}return[7];case 8:p=this.plugins.filter((function(e){return"enrichment"===e.type||void 0===e.type})),o.label=9;case 9:o.trys.push([9,14,15,16]),v=s(p),h=v.next(),o.label=10;case 10:return h.done?[3,13]:(g=h.value).execute?[4,g.execute(n({},i))]:[3,12];case 11:if(null===(y=o.sent()))return a({event:i,code:0,message:""}),[2];i=y,o.label=12;case 12:return h=v.next(),[3,10];case 13:return[3,16];case 14:return m=o.sent(),S={error:m},[3,16];case 15:try{h&&!h.done&&(E=v.return)&&E.call(v)}finally{if(S)throw S.error}return[7];case 16:return b=this.plugins.filter((function(e){return"destination"===e.type})),_=b.map((function(e){var t=n({},i);return e.execute(t).catch((function(e){return D(t,0,String(e))}))})),Promise.all(_).then((function(e){var t=u(e,1)[0]||D(i,100,"Event not tracked, no destination plugins on the instance");a(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,n,i=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return i.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return"destination"===e.type})),n=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(n)];case 2:return r.sent(),[2]}}))}))},e}(),A="AMP",j="".concat(A,"_unsent"),C="https://api2.amplitude.com/2/httpapi",M=function(e){if(Object.keys(e).length>1e3)return!1;for(var t in e){var n=e[t];if(!V(t,n))return!1}return!0},V=function(e,t){var n,i;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),u=o.next();!u.done;u=o.next()){var a=u.value;if(Array.isArray(a))return!1;if("object"==typeof a)r=r&&M(a);else if(!["number","string"].includes(typeof a))return!1;if(!r)return!1}}catch(e){n={error:e}}finally{try{u&&!u.done&&(i=o.return)&&i.call(o)}finally{if(n)throw n.error}}}else{if(null==t)return!1;if("object"==typeof t)return M(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},F=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return n({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,n){if(this._validate(e,t,n)){var i=this._properties[e];return void 0===i&&(i={},this._properties[e]=i),i[t]=n,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,n){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof n:e===c.UNSET||e===c.REMOVE||V(t,n)))},e}(),Q=function(e,t){return n(n({},t),{event_type:d.IDENTIFY,user_properties:e.getUserProperties()})},$=function(e){return{promise:e||Promise.resolve()}},K=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new L(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,n,i,r,u,a;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),n=s(t),i=n.next(),o.label=2;case 2:return i.done?[3,5]:[4,(0,i.value)()];case 3:o.sent(),o.label=4;case 4:return i=n.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),u={error:r},[3,8];case 7:try{i&&!i.done&&(a=n.return)&&a.call(n)}finally{if(u)throw u.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,i){var r=function(e,t,i){return n(n(n({},"string"==typeof e?{event_type:e}:e),i),t&&{event_properties:t})}(e,t,i);return $(this.dispatch(r))},e.prototype.identify=function(e,t){var n=Q(e,t);return $(this.dispatch(n))},e.prototype.groupIdentify=function(e,t,i,r){var o=function(e,t,i,r){var o;return n(n({},r),{event_type:d.GROUP_IDENTIFY,group_properties:i.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,i,r);return $(this.dispatch(o))},e.prototype.setGroup=function(e,t,i){var r=function(e,t,i){var r,o=new F;return o.set(e,t),n(n({},i),{event_type:d.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,i);return $(this.dispatch(r))},e.prototype.revenue=function(e,t){var i=function(e,t){return n(n({},t),{event_type:d.REVENUE,event_properties:e.getEventProperties()})}(e,t);return $(this.dispatch(i))},e.prototype.add=function(e){return this.config?$(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),$())},e.prototype.remove=function(e){return this.config?$(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),$())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(D(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(n){return this.config?[2,this.process(e)]:[2,new Promise((function(n){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,n))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,n,i;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,D(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(i=r.sent()).code?this.config.loggerProvider.log(i.message):100===i.code?this.config.loggerProvider.warn(i.message):this.config.loggerProvider.error(i.message),[2,i];case 2:return t=r.sent(),n=String(t),this.config.loggerProvider.error(n),[2,i=D(e,0,n)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return $(this.timeline.flush())},e}(),B=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return M(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?n({},this.properties):{};return e[l.REVENUE_PRODUCT_ID]=this.productId,e[l.REVENUE_QUANTITY]=this.quantity,e[l.REVENUE_PRICE]=this.price,e[l.REVENUE_TYPE]=this.revenueType,e[l.REVENUE]=this.revenue,e},e}(),z="Amplitude Logger ",W=function(){function e(){this.logLevel=f.None}return e.prototype.disable=function(){this.logLevel=f.None},e.prototype.enable=function(e){void 0===e&&(e=f.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(n.map((function(e){return i.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(n){var i={event:e,attempts:0,callback:function(e){return n(e)},timeout:0};t.addToQueue(i)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],n=0;n0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,n,i,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],n=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):n.push(e)})),this.queue=n,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,u=this.config.flushQueueSize,a=Math.max(u,1),i=s.reduce((function(e,t,n){var i=Math.floor(n/a);return e[i]||(e[i]=[]),e[i].push(t),e}),[]),[4,Promise.all(i.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,u,a}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var n,r,s,u,a;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];n={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,i(t,["extra"])})),options:{min_id_length:this.config.minIdLength},client_upload_time:(new Date).toISOString()},o.label=1;case 1:return o.trys.push([1,3,,4]),r=G(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,n)];case 2:return null===(s=o.sent())?(this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(s,e),[3,4]):("body"in s?this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(H(s))):this.fulfillRequest(e,s.statusCode,s.status),[2]);case 3:return u=o.sent(),a=(c=u)instanceof Error?c.message:String(c),this.config.loggerProvider.error(a),this.fulfillRequest(e,0,a),[3,4];case 4:return[2]}var c}))}))},e.prototype.handleResponse=function(e,t){var n=e.status;switch(n){case v.Success:this.handleSuccessResponse(e,t);break;case v.Invalid:this.handleInvalidResponse(e,t);break;case v.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case v.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(n,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var n=this;if(e.body.missingField||e.body.error.startsWith("Invalid API key"))this.fulfillRequest(t,e.statusCode,e.body.error);else{var i=a(a(a(a([],u(Object.values(e.body.eventsWithInvalidFields)),!1),u(Object.values(e.body.eventsWithMissingFields)),!1),u(Object.values(e.body.eventsWithInvalidIdLengths)),!1),u(e.body.silencedEvents),!1).flat(),r=new Set(i),o=t.filter((function(t,i){if(!r.has(i))return!0;n.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(H(e)),this.addToQueue.apply(this,a([],u(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){1!==t.length?(this.config.loggerProvider.warn(H(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,a([],u(t),!1))):this.fulfillRequest(t,e.statusCode,e.body.error)},e.prototype.handleRateLimitResponse=function(e,t){var n=this,i=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(i),c=new Set(r),l=new Set(o),d=t.filter((function(t,i){if(!(t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)))return l.has(i)&&(t.timeout=n.throttleTimeout),!0;n.fulfillRequest([t],e.statusCode,e.body.error)}));d.length>0&&this.config.loggerProvider.warn(H(e)),this.addToQueue.apply(this,a([],u(d),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,a([],u(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,n){this.saveEvents(),e.forEach((function(e){return e.callback(D(e.event,t,n))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ee=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},te=function(e){return function(){var t=n({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},ne=function(e,t){var n,i;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var u=o.value;if(!(u in e))return;e=e[u]}}catch(e){n={error:e}}finally{try{o&&!o.done&&(i=r.return)&&i.call(r)}finally{if(n)throw n.error}}return e},ie=function(e,t){return function(){var n,i,r={};try{for(var o=s(t),u=o.next();!u.done;u=o.next()){var a=u.value;r[a]=ne(e,a)}}catch(e){n={error:e}}finally{try{u&&!u.done&&(i=o.return)&&i.call(o)}finally{if(n)throw n.error}}return r}},re=function(e,t,n,i,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s=200&&e<300?v.Success:429===e?v.RateLimit:413===e?v.PayloadTooLarge:408===e?v.Timeout:e>=400&&e<500?v.Invalid:e>=500?v.Failed:v.Unknown},e}(),ue=function(e,t,n){return void 0===t&&(t=""),void 0===n&&(n=10),[A,t,e.substring(0,n)].filter(Boolean).join("_")},ae=function(e,t){return void 0===t&&(t=Date.now()),Date.now()-t>e},ce=function(){function e(e){this.options=n({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,n;return o(this,(function(i){switch(i.label){case 0:if(!h())return[2,!1];e.testValue=String(Date.now()),t=new e(this.options),n="AMP_TEST",i.label=1;case 1:return i.trys.push([1,4,5,7]),[4,t.set(n,e.testValue)];case 2:return i.sent(),[4,t.get(n)];case 3:return[2,i.sent()===e.testValue];case 4:return i.sent(),[2,!1];case 5:return[4,t.remove(n)];case 6:return i.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(n){switch(n.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=n.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t,n;return r(this,void 0,void 0,(function(){var i,r,s;return o(this,(function(o){return i=h(),r=null!==(n=null===(t=null==i?void 0:i.document)||void 0===t?void 0:t.cookie.split("; "))&&void 0!==n?n:[],(s=r.find((function(t){return 0===t.indexOf(e+"=")})))?[2,s.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var n;return r(this,void 0,void 0,(function(){var i,r,s,u,a,c;return o(this,(function(o){try{i=null!==(n=this.options.expirationDays)&&void 0!==n?n:0,s=void 0,(r=null!==t?i:-1)&&((u=new Date).setTime(u.getTime()+24*r*60*60*1e3),s=u),a="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(a+="; expires=".concat(s.toUTCString())),a+="; path=/",this.options.domain&&(a+="; domain=".concat(this.options.domain)),this.options.secure&&(a+="; Secure"),this.options.sameSite&&(a+="; SameSite=".concat(this.options.sameSite)),(c=h())&&(c.document.cookie=a)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),le=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var n,i;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return n={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,n)];case 1:return[4,r.sent().json()];case 2:return i=r.sent(),[2,this.buildResponse(i)]}}))}))},n}(se),de=function(){function e(){}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:fe(),platform:"Web",os:void 0,deviceModel:void 0}},e}(),fe=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},pe=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),ve=function(){return ve=Object.assign||function(e){for(var t,n=1,i=arguments.length;nxe?(r=n.length-xe,[4,e.prototype.set.call(this,t,n.slice(0,xe))]):[3,2];case 1:return o.sent(),null===(i=this.loggerProvider)||void 0===i||i.error("Failed to save ".concat(r," events because the queue length exceeded ").concat(xe,".")),[3,4];case 2:return[4,e.prototype.set.call(this,t,n)];case 3:o.sent(),o.label=4;case 4:return[2]}}))}))},n}(Re),qe=function(e){function n(){var t;return e.call(this,null===(t=h())||void 0===t?void 0:t.sessionStorage)||this}return t(n,e),n}(Re),De=function(e){function n(){var t=null!==e&&e.apply(this,arguments)||this;return t.state={done:4},t}return t(n,e),n.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var n=this;return o(this,(function(i){return[2,new Promise((function(i,r){"undefined"==typeof XMLHttpRequest&&r(new Error("XHRTransport is not supported."));var o=new XMLHttpRequest;o.open("POST",e,!0),o.onreadystatechange=function(){if(o.readyState===n.state.done)try{var e=o.responseText,t=JSON.parse(e),s=n.buildResponse(t);i(s)}catch(e){r(e)}},o.setRequestHeader("Content-Type","application/json"),o.setRequestHeader("Accept","*/*"),o.send(JSON.stringify(t))}))]}))}))},n}(se),Ne=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var n=this;return o(this,(function(i){return[2,new Promise((function(i,r){var o=h();if(!(null==o?void 0:o.navigator.sendBeacon))throw new Error("SendBeaconTransport is not supported");try{var s=JSON.stringify(t);return i(o.navigator.sendBeacon(e,JSON.stringify(t))?n.buildResponse({code:200,events_ingested:t.events.length,payload_size_bytes:s.length,server_upload_time:Date.now()}):n.buildResponse({code:500}))}catch(e){r(e)}}))]}))}))},n}(se),Le=function(e,t,n){return void 0===n&&(n=!0),r(void 0,void 0,void 0,(function(){var i,r,s,a,c,l,d,f,p;return o(this,(function(o){switch(o.label){case 0:return i=function(e){return"".concat(A.toLowerCase(),"_").concat(e.substring(0,6))}(e),[4,t.getRaw(i)];case 1:return(r=o.sent())?n?[4,t.remove(i)]:[3,3]:[2,{optOut:!1}];case 2:o.sent(),o.label=3;case 3:return s=u(r.split("."),6),a=s[0],c=s[1],l=s[2],d=s[3],f=s[4],p=s[5],[2,{deviceId:a,userId:je(c),sessionId:Ae(d),lastEventId:Ae(p),lastEventTime:Ae(f),optOut:Boolean(l)}]}}))}))},Ae=function(e){var t=parseInt(e,32);if(!isNaN(t))return t},je=function(e){if(atob&&escape&&e)try{return decodeURIComponent(escape(atob(e)))}catch(e){return}},Ce="[Amplitude]",Me="".concat(Ce," Form Started"),Ve="".concat(Ce," Form Submitted"),Fe="".concat(Ce," File Downloaded"),Qe="session_start",$e="session_end",Ke="".concat(Ce," File Extension"),Be="".concat(Ce," File Name"),ze="".concat(Ce," Link ID"),We="".concat(Ce," Link Text"),Je="".concat(Ce," Link URL"),Ze="".concat(Ce," Form ID"),Ye="".concat(Ce," Form Name"),Ge="".concat(Ce," Form Destination"),He="cookie",Xe=function(e){function n(t,n,i,r,o,s,u,a,c,l,d,p,v,h,g,y,m,b,_,I,w,S,E,T,O,k,P,R,x){void 0===i&&(i=new oe),void 0===r&&(r={domain:"",expiration:365,sameSite:"Lax",secure:!1,upgrade:!0}),void 0===u&&(u=1e3),void 0===a&&(a=5),void 0===c&&(c=30),void 0===l&&(l=He),void 0===g&&(g=new W),void 0===y&&(y=f.Warn),void 0===b&&(b=!1),void 0===w&&(w=""),void 0===S&&(S="US"),void 0===T&&(T=18e5),void 0===O&&(O=new Ue({loggerProvider:g})),void 0===k&&(k={ipAddress:!0,language:!0,platform:!0}),void 0===P&&(P="fetch"),void 0===R&&(R=!1);var U=e.call(this,{apiKey:t,storageProvider:O,transportProvider:nt(P)})||this;return U.apiKey=t,U.appVersion=n,U.cookieOptions=r,U.defaultTracking=o,U.flushIntervalMillis=u,U.flushMaxRetries=a,U.flushQueueSize=c,U.identityStorage=l,U.ingestionMetadata=d,U.instanceName=p,U.loggerProvider=g,U.logLevel=y,U.minIdLength=m,U.partnerId=_,U.plan=I,U.serverUrl=w,U.serverZone=S,U.sessionTimeout=T,U.storageProvider=O,U.trackingOptions=k,U.transport=P,U.useBatch=R,U._optOut=!1,U._cookieStorage=i,U.deviceId=s,U.lastEventId=v,U.lastEventTime=h,U.optOut=b,U.sessionId=E,U.userId=x,U.loggerProvider.enable(U.logLevel),U}return t(n,e),Object.defineProperty(n.prototype,"cookieStorage",{get:function(){return this._cookieStorage},set:function(e){this._cookieStorage!==e&&(this._cookieStorage=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"deviceId",{get:function(){return this._deviceId},set:function(e){this._deviceId!==e&&(this._deviceId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"userId",{get:function(){return this._userId},set:function(e){this._userId!==e&&(this._userId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"sessionId",{get:function(){return this._sessionId},set:function(e){this._sessionId!==e&&(this._sessionId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"optOut",{get:function(){return this._optOut},set:function(e){this._optOut!==e&&(this._optOut=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"lastEventTime",{get:function(){return this._lastEventTime},set:function(e){this._lastEventTime!==e&&(this._lastEventTime=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"lastEventId",{get:function(){return this._lastEventId},set:function(e){this._lastEventId!==e&&(this._lastEventId=e,this.updateStorage())},enumerable:!1,configurable:!0}),n.prototype.updateStorage=function(){var e={deviceId:this._deviceId,userId:this._userId,sessionId:this._sessionId,optOut:this._optOut,lastEventTime:this._lastEventTime,lastEventId:this._lastEventId};this.cookieStorage.set(ue(this.apiKey),e)},n}(Z),et=function(e,t,i){return void 0===t&&(t={}),r(void 0,void 0,void 0,(function(){var r,s,u,a,c,l,d,f,p,v,h,y,m,b,_,I,w,S,E,T,O,k,P,R,x,U,q,D,L,A,j,C,M,V,F,Q,$,K,B;return o(this,(function(o){switch(o.label){case 0:return r=t.identityStorage||He,I={},r===He?[3,1]:(u="",[3,5]);case 1:return null===(S=null===(w=t.cookieOptions)||void 0===w?void 0:w.domain)||void 0===S?[3,2]:(a=S,[3,4]);case 2:return[4,it()];case 3:a=o.sent(),o.label=4;case 4:u=a,o.label=5;case 5:return s=n.apply(void 0,[(I.domain=u,I.expiration=365,I.sameSite="Lax",I.secure=!1,I.upgrade=!0,I),t.cookieOptions]),c=tt(t.identityStorage,s),[4,Le(e,c,null===(T=null===(E=t.cookieOptions)||void 0===E?void 0:E.upgrade)||void 0===T||T)];case 6:return l=o.sent(),[4,c.get(ue(e))];case 7:return d=o.sent(),f=g(),p=null!==(R=null!==(P=null!==(k=null!==(O=t.deviceId)&&void 0!==O?O:f.deviceId)&&void 0!==k?k:null==d?void 0:d.deviceId)&&void 0!==P?P:l.deviceId)&&void 0!==R?R:N(),v=null!==(x=null==d?void 0:d.lastEventId)&&void 0!==x?x:l.lastEventId,h=null!==(U=null==d?void 0:d.lastEventTime)&&void 0!==U?U:l.lastEventTime,y=null!==(D=null!==(q=t.optOut)&&void 0!==q?q:null==d?void 0:d.optOut)&&void 0!==D?D:l.optOut,m=null!==(L=null==d?void 0:d.sessionId)&&void 0!==L?L:l.sessionId,b=null!==(j=null!==(A=t.userId)&&void 0!==A?A:null==d?void 0:d.userId)&&void 0!==j?j:l.userId,i.previousSessionDeviceId=null!==(C=null==d?void 0:d.deviceId)&&void 0!==C?C:l.deviceId,i.previousSessionUserId=null!==(M=null==d?void 0:d.userId)&&void 0!==M?M:l.userId,_={ipAddress:null===(F=null===(V=t.trackingOptions)||void 0===V?void 0:V.ipAddress)||void 0===F||F,language:null===($=null===(Q=t.trackingOptions)||void 0===Q?void 0:Q.language)||void 0===$||$,platform:null===(B=null===(K=t.trackingOptions)||void 0===K?void 0:K.platform)||void 0===B||B},[2,new Xe(e,t.appVersion,c,s,t.defaultTracking,p,t.flushIntervalMillis,t.flushMaxRetries,t.flushQueueSize,r,t.ingestionMetadata,t.instanceName,v,h,t.loggerProvider,t.logLevel,t.minIdLength,y,t.partnerId,t.plan,t.serverUrl,t.serverZone,m,t.sessionTimeout,t.storageProvider,_,t.transport,t.useBatch,b)]}}))}))},tt=function(e,t){switch(void 0===e&&(e=He),void 0===t&&(t={}),e){case"localStorage":return new Ue;case"sessionStorage":return new qe;case"none":return new oe;default:return new ce(n(n({},t),{expirationDays:t.expiration}))}},nt=function(e){return"xhr"===e?new De:"beacon"===e?new Ne:new le},it=function(e){return r(void 0,void 0,void 0,(function(){var t,n,i,r,s,u,a;return o(this,(function(o){switch(o.label){case 0:return[4,(new ce).isEnabled()];case 1:if(!o.sent()||!e&&"undefined"==typeof location)return[2,""];for(t=null!=e?e:location.hostname,n=t.split("."),i=[],r="AMP_TLDTEST",s=n.length-2;s>=0;--s)i.push(n.slice(s).join("."));s=0,o.label=2;case 2:return s2?(r=t,o=i):"string"==typeof t?(r=t,o=void 0):(r=null==t?void 0:t.userId,o=t),$(this._init(n(n({},o),{userId:r,apiKey:e})))},i.prototype._init=function(t){var i,s;return r(this,void 0,void 0,(function(){var r,u,a,c,l=this;return o(this,(function(o){switch(o.label){case 0:return this.initializing?[2]:(this.initializing=!0,[4,et(t.apiKey,t,this)]);case 1:return r=o.sent(),[4,e.prototype._init.call(this,r)];case 2:return o.sent(),this.setSessionId(null!==(s=null!==(i=t.sessionId)&&void 0!==i?i:this.config.sessionId)&&void 0!==s?s:Date.now()),(u=be(t.instanceName)).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new X).promise];case 3:return o.sent(),[4,this.add(new Pe).promise];case 4:return o.sent(),[4,this.add(new _e).promise];case 5:return o.sent(),f=this.config,gt||void 0!==f.defaultTracking||(f.loggerProvider.warn("`options.defaultTracking` is set to undefined. This implicitly configures your Amplitude instance to track Page Views, Sessions, File Downloads, and Form Interactions. You can suppress this warning by explicitly setting a value to `options.defaultTracking`. The value must either be a boolean, to enable and disable all default events, or an object, for advanced configuration. For example:\n\namplitude.init(, {\n defaultTracking: true,\n});\n\nVisit https://www.docs.developers.amplitude.com/data/sdks/browser-2/#tracking-default-events for more details."),gt=!0),d=this.config.defaultTracking,we(d,"fileDownloads")?[4,this.add(ht()).promise]:[3,7];case 6:o.sent(),o.label=7;case 7:return function(e){return we(e,"formInteractions")}(this.config.defaultTracking)?[4,this.add(pt()).promise]:[3,9];case 8:o.sent(),o.label=9;case 9:return Se(this.config.defaultTracking)?(a=function(e){return Se(e.defaultTracking)&&e.defaultTracking&&"object"==typeof e.defaultTracking&&e.defaultTracking.attribution&&"object"==typeof e.defaultTracking.attribution?n({},e.defaultTracking.attribution):{}}(this.config),c=ut(a),[4,this.add(c).promise]):[3,11];case 10:o.sent(),o.label=11;case 11:return[4,this.add(ct(Ee(this.config))).promise];case 12:return o.sent(),this.initializing=!1,[4,this.runQueuedFunctions("dispatchQ")];case 13:return o.sent(),u.eventBridge.setEventReceiver((function(e){l.track(e.eventType,e.eventProperties)})),[2]}var d,f}))}))},i.prototype.getUserId=function(){var e;return null===(e=this.config)||void 0===e?void 0:e.userId},i.prototype.setUserId=function(e){this.config?e===this.config.userId&&void 0!==e||(this.config.userId=e,function(e,t){be(t).identityStore.editIdentity().setUserId(e).commit()}(e,this.config.instanceName)):this.q.push(this.setUserId.bind(this,e))},i.prototype.getDeviceId=function(){var e;return null===(e=this.config)||void 0===e?void 0:e.deviceId},i.prototype.setDeviceId=function(e){this.config?(this.config.deviceId=e,function(e,t){be(t).identityStore.editIdentity().setDeviceId(e).commit()}(e,this.config.instanceName)):this.q.push(this.setDeviceId.bind(this,e))},i.prototype.reset=function(){this.setDeviceId(N()),this.setUserId(void 0)},i.prototype.getSessionId=function(){var e;return null===(e=this.config)||void 0===e?void 0:e.sessionId},i.prototype.setSessionId=function(e){var t;if(this.config){if(e!==this.config.sessionId){var n,i=this.getSessionId(),r=this.config.lastEventTime,o=null!==(t=this.config.lastEventId)&&void 0!==t?t:-1;this.config.sessionId=e,this.config.lastEventTime=void 0,n=this.config.defaultTracking,we(n,"sessions")&&(i&&r&&this.track($e,void 0,{device_id:this.previousSessionDeviceId,event_id:++o,session_id:i,time:r+1,user_id:this.previousSessionUserId}),this.config.lastEventTime=this.config.sessionId,this.track(Qe,void 0,{event_id:++o,session_id:this.config.sessionId,time:this.config.lastEventTime})),this.previousSessionDeviceId=this.config.deviceId,this.previousSessionUserId=this.config.userId}}else this.q.push(this.setSessionId.bind(this,e))},i.prototype.extendSession=function(){this.config?this.config.lastEventTime=Date.now():this.q.push(this.extendSession.bind(this))},i.prototype.setTransport=function(e){this.config?this.config.transportProvider=nt(e):this.q.push(this.setTransport.bind(this,e))},i.prototype.identify=function(t,n){if(ke(t)){var i=t._q;t._q=[],t=Oe(new F,i)}return(null==n?void 0:n.user_id)&&this.setUserId(n.user_id),(null==n?void 0:n.device_id)&&this.setDeviceId(n.device_id),e.prototype.identify.call(this,t,n)},i.prototype.groupIdentify=function(t,n,i,r){if(ke(i)){var o=i._q;i._q=[],i=Oe(new F,o)}return e.prototype.groupIdentify.call(this,t,n,i,r)},i.prototype.revenue=function(t,n){if(ke(t)){var i=t._q;t._q=[],t=Oe(new B,i)}return e.prototype.revenue.call(this,t,n)},i.prototype.process=function(t){return r(this,void 0,void 0,(function(){var n,i;return o(this,(function(r){return n=Date.now(),i=ae(this.config.sessionTimeout,this.config.lastEventTime),t.event_type===Qe||t.event_type===$e||t.session_id&&t.session_id!==this.getSessionId()||!i||this.setSessionId(n),[2,e.prototype.process.call(this,t)]}))}))},i}(K),mt=function(){var e=new yt;return{init:re(e.init.bind(e),"init",te(e),ie(e,["config"])),add:re(e.add.bind(e),"add",te(e),ie(e,["config.apiKey","timeline.plugins"])),remove:re(e.remove.bind(e),"remove",te(e),ie(e,["config.apiKey","timeline.plugins"])),track:re(e.track.bind(e),"track",te(e),ie(e,["config.apiKey","timeline.queue.length"])),logEvent:re(e.logEvent.bind(e),"logEvent",te(e),ie(e,["config.apiKey","timeline.queue.length"])),identify:re(e.identify.bind(e),"identify",te(e),ie(e,["config.apiKey","timeline.queue.length"])),groupIdentify:re(e.groupIdentify.bind(e),"groupIdentify",te(e),ie(e,["config.apiKey","timeline.queue.length"])),setGroup:re(e.setGroup.bind(e),"setGroup",te(e),ie(e,["config.apiKey","timeline.queue.length"])),revenue:re(e.revenue.bind(e),"revenue",te(e),ie(e,["config.apiKey","timeline.queue.length"])),flush:re(e.flush.bind(e),"flush",te(e),ie(e,["config.apiKey","timeline.queue.length"])),getUserId:re(e.getUserId.bind(e),"getUserId",te(e),ie(e,["config","config.userId"])),setUserId:re(e.setUserId.bind(e),"setUserId",te(e),ie(e,["config","config.userId"])),getDeviceId:re(e.getDeviceId.bind(e),"getDeviceId",te(e),ie(e,["config","config.deviceId"])),setDeviceId:re(e.setDeviceId.bind(e),"setDeviceId",te(e),ie(e,["config","config.deviceId"])),reset:re(e.reset.bind(e),"reset",te(e),ie(e,["config","config.userId","config.deviceId"])),getSessionId:re(e.getSessionId.bind(e),"getSessionId",te(e),ie(e,["config"])),setSessionId:re(e.setSessionId.bind(e),"setSessionId",te(e),ie(e,["config"])),extendSession:re(e.extendSession.bind(e),"extendSession",te(e),ie(e,["config"])),setOptOut:re(e.setOptOut.bind(e),"setOptOut",te(e),ie(e,["config"])),setTransport:re(e.setTransport.bind(e),"setTransport",te(e),ie(e,["config"]))}},bt=mt(),_t=bt.add,It=bt.extendSession,wt=bt.flush,St=bt.getDeviceId,Et=bt.getSessionId,Tt=bt.getUserId,Ot=bt.groupIdentify,kt=bt.identify,Pt=bt.init,Rt=bt.logEvent,xt=bt.remove,Ut=bt.reset,qt=bt.revenue,Dt=bt.setDeviceId,Nt=bt.setGroup,Lt=bt.setOptOut,At=bt.setSessionId,jt=bt.setTransport,Ct=bt.setUserId,Mt=bt.track,Vt=Object.freeze({__proto__:null,add:_t,extendSession:It,flush:wt,getDeviceId:St,getSessionId:Et,getUserId:Tt,groupIdentify:Ot,identify:kt,init:Pt,logEvent:Rt,remove:xt,reset:Ut,revenue:qt,setDeviceId:Dt,setGroup:Nt,setOptOut:Lt,setSessionId:At,setTransport:jt,setUserId:Ct,track:Mt,Types:q,createInstance:mt,runQueuedFunctions:Te,Revenue:B,Identify:F});!function(){var e=h();if(e){var t=function(e){var t=mt(),n=h();return n&&n.amplitude&&n.amplitude._iq&&e&&(n.amplitude._iq[e]=t),t};if(e.amplitude=Object.assign(e.amplitude||{},Vt,{createInstance:t}),e.amplitude.invoked){var n=e.amplitude._q;e.amplitude._q=[],Te(Vt,n);for(var i=Object.keys(e.amplitude._iq)||[],r=0;r0&&r[r.length-1])||6!==u[0]&&2!==u[0])){s=0;continue}if(3===u[0]&&(!r||u[1]>r[0]&&u[1]=e.length&&(e=void 0),{value:e&&e[i++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function u(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var i,r,o=n.call(e),s=[];try{for(;(void 0===t||t-- >0)&&!(i=o.next()).done;)s.push(i.value)}catch(e){r={error:e}}finally{try{i&&!i.done&&(n=o.return)&&n.call(o)}finally{if(r)throw r.error}}return s}function a(e,t,n){if(n||2===arguments.length)for(var i,r=0,o=t.length;r>e/4).toString(16):(String(1e7)+String(-1e3)+String(-4e3)+String(-8e3)+String(-1e11)).replace(/[018]/g,L)},A=function(){function e(e){this.client=e,this.queue=[],this.applying=!1,this.plugins=[]}return e.prototype.register=function(e,t){var n,i,s;return r(this,void 0,void 0,(function(){return o(this,(function(r){switch(r.label){case 0:return e.name=null!==(n=e.name)&&void 0!==n?n:L(),e.type=null!==(i=e.type)&&void 0!==i?i:"enrichment",[4,null===(s=e.setup)||void 0===s?void 0:s.call(e,t,this.client)];case 1:return r.sent(),this.plugins.push(e),[2]}}))}))},e.prototype.deregister=function(e){var t;return r(this,void 0,void 0,(function(){var n,i;return o(this,(function(r){switch(r.label){case 0:return n=this.plugins.findIndex((function(t){return t.name===e})),i=this.plugins[n],this.plugins.splice(n,1),[4,null===(t=i.teardown)||void 0===t?void 0:t.call(i)];case 1:return r.sent(),[2]}}))}))},e.prototype.reset=function(e){this.applying=!1,this.plugins.map((function(e){var t;return null===(t=e.teardown)||void 0===t?void 0:t.call(e)})),this.plugins=[],this.client=e},e.prototype.push=function(e){var t=this;return new Promise((function(n){t.queue.push([e,n]),t.scheduleApply(0)}))},e.prototype.scheduleApply=function(e){var t=this;this.applying||(this.applying=!0,setTimeout((function(){t.apply(t.queue.shift()).then((function(){t.applying=!1,t.queue.length>0&&t.scheduleApply(0)}))}),e))},e.prototype.apply=function(e){return r(this,void 0,void 0,(function(){var t,i,r,a,c,d,l,f,p,v,h,g,y,m,b,_,I,w,S,E;return o(this,(function(o){switch(o.label){case 0:if(!e)return[2];t=u(e,1),i=t[0],r=u(e,2),a=r[1],c=this.plugins.filter((function(e){return"before"===e.type})),o.label=1;case 1:o.trys.push([1,6,7,8]),d=s(c),l=d.next(),o.label=2;case 2:return l.done?[3,5]:(g=l.value).execute?[4,g.execute(n({},i))]:[3,4];case 3:if(null===(y=o.sent()))return a({event:i,code:0,message:""}),[2];i=y,o.label=4;case 4:return l=d.next(),[3,2];case 5:return[3,8];case 6:return f=o.sent(),I={error:f},[3,8];case 7:try{l&&!l.done&&(w=d.return)&&w.call(d)}finally{if(I)throw I.error}return[7];case 8:p=this.plugins.filter((function(e){return"enrichment"===e.type||void 0===e.type})),o.label=9;case 9:o.trys.push([9,14,15,16]),v=s(p),h=v.next(),o.label=10;case 10:return h.done?[3,13]:(g=h.value).execute?[4,g.execute(n({},i))]:[3,12];case 11:if(null===(y=o.sent()))return a({event:i,code:0,message:""}),[2];i=y,o.label=12;case 12:return h=v.next(),[3,10];case 13:return[3,16];case 14:return m=o.sent(),S={error:m},[3,16];case 15:try{h&&!h.done&&(E=v.return)&&E.call(v)}finally{if(S)throw S.error}return[7];case 16:return b=this.plugins.filter((function(e){return"destination"===e.type})),_=b.map((function(e){var t=n({},i);return e.execute(t).catch((function(e){return N(t,0,String(e))}))})),Promise.all(_).then((function(e){var t=u(e,1)[0]||N(i,100,"Event not tracked, no destination plugins on the instance");a(t)})),[2]}}))}))},e.prototype.flush=function(){return r(this,void 0,void 0,(function(){var e,t,n,i=this;return o(this,(function(r){switch(r.label){case 0:return e=this.queue,this.queue=[],[4,Promise.all(e.map((function(e){return i.apply(e)})))];case 1:return r.sent(),t=this.plugins.filter((function(e){return"destination"===e.type})),n=t.map((function(e){return e.flush&&e.flush()})),[4,Promise.all(n)];case 2:return r.sent(),[2]}}))}))},e}(),j="AMP",C="".concat(j,"_unsent"),M="https://api2.amplitude.com/2/httpapi",V=function(e){if(Object.keys(e).length>1e3)return!1;for(var t in e){var n=e[t];if(!F(t,n))return!1}return!0},F=function(e,t){var n,i;if("string"!=typeof e)return!1;if(Array.isArray(t)){var r=!0;try{for(var o=s(t),u=o.next();!u.done;u=o.next()){var a=u.value;if(Array.isArray(a))return!1;if("object"==typeof a)r=r&&V(a);else if(!["number","string"].includes(typeof a))return!1;if(!r)return!1}}catch(e){n={error:e}}finally{try{u&&!u.done&&(i=o.return)&&i.call(o)}finally{if(n)throw n.error}}}else{if(null==t)return!1;if("object"==typeof t)return V(t);if(!["number","string","boolean"].includes(typeof t))return!1}return!0},K=function(){function e(){this._propertySet=new Set,this._properties={}}return e.prototype.getUserProperties=function(){return n({},this._properties)},e.prototype.set=function(e,t){return this._safeSet(c.SET,e,t),this},e.prototype.setOnce=function(e,t){return this._safeSet(c.SET_ONCE,e,t),this},e.prototype.append=function(e,t){return this._safeSet(c.APPEND,e,t),this},e.prototype.prepend=function(e,t){return this._safeSet(c.PREPEND,e,t),this},e.prototype.postInsert=function(e,t){return this._safeSet(c.POSTINSERT,e,t),this},e.prototype.preInsert=function(e,t){return this._safeSet(c.PREINSERT,e,t),this},e.prototype.remove=function(e,t){return this._safeSet(c.REMOVE,e,t),this},e.prototype.add=function(e,t){return this._safeSet(c.ADD,e,t),this},e.prototype.unset=function(e){return this._safeSet(c.UNSET,e,"-"),this},e.prototype.clearAll=function(){return this._properties={},this._properties[c.CLEAR_ALL]="-",this},e.prototype._safeSet=function(e,t,n){if(this._validate(e,t,n)){var i=this._properties[e];return void 0===i&&(i={},this._properties[e]=i),i[t]=n,this._propertySet.add(t),!0}return!1},e.prototype._validate=function(e,t,n){return void 0===this._properties[c.CLEAR_ALL]&&(!this._propertySet.has(t)&&(e===c.ADD?"number"==typeof n:e===c.UNSET||e===c.REMOVE||F(t,n)))},e}(),Q=function(e,t){return n(n({},t),{event_type:l.IDENTIFY,user_properties:e.getUserProperties()})},$=function(e){return{promise:e||Promise.resolve()}},B=function(){function e(e){void 0===e&&(e="$default"),this.initializing=!1,this.q=[],this.dispatchQ=[],this.logEvent=this.track.bind(this),this.timeline=new A(this),this.name=e}return e.prototype._init=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return this.config=e,this.timeline.reset(this),[4,this.runQueuedFunctions("q")];case 1:return t.sent(),[2]}}))}))},e.prototype.runQueuedFunctions=function(e){return r(this,void 0,void 0,(function(){var t,n,i,r,u,a;return o(this,(function(o){switch(o.label){case 0:t=this[e],this[e]=[],o.label=1;case 1:o.trys.push([1,6,7,8]),n=s(t),i=n.next(),o.label=2;case 2:return i.done?[3,5]:[4,(0,i.value)()];case 3:o.sent(),o.label=4;case 4:return i=n.next(),[3,2];case 5:return[3,8];case 6:return r=o.sent(),u={error:r},[3,8];case 7:try{i&&!i.done&&(a=n.return)&&a.call(n)}finally{if(u)throw u.error}return[7];case 8:return[2]}}))}))},e.prototype.track=function(e,t,i){var r=function(e,t,i){return n(n(n({},"string"==typeof e?{event_type:e}:e),i),t&&{event_properties:t})}(e,t,i);return $(this.dispatch(r))},e.prototype.identify=function(e,t){var n=Q(e,t);return $(this.dispatch(n))},e.prototype.groupIdentify=function(e,t,i,r){var o=function(e,t,i,r){var o;return n(n({},r),{event_type:l.GROUP_IDENTIFY,group_properties:i.getUserProperties(),groups:(o={},o[e]=t,o)})}(e,t,i,r);return $(this.dispatch(o))},e.prototype.setGroup=function(e,t,i){var r=function(e,t,i){var r,o=new K;return o.set(e,t),n(n({},i),{event_type:l.IDENTIFY,user_properties:o.getUserProperties(),groups:(r={},r[e]=t,r)})}(e,t,i);return $(this.dispatch(r))},e.prototype.revenue=function(e,t){var i=function(e,t){return n(n({},t),{event_type:l.REVENUE,event_properties:e.getEventProperties()})}(e,t);return $(this.dispatch(i))},e.prototype.add=function(e){return this.config?$(this.timeline.register(e,this.config)):(this.q.push(this.add.bind(this,e)),$())},e.prototype.remove=function(e){return this.config?$(this.timeline.deregister(e)):(this.q.push(this.remove.bind(this,e)),$())},e.prototype.dispatchWithCallback=function(e,t){if(!this.config)return t(N(e,0,"Client not initialized"));this.process(e).then(t)},e.prototype.dispatch=function(e){return r(this,void 0,void 0,(function(){var t=this;return o(this,(function(n){return this.config?[2,this.process(e)]:[2,new Promise((function(n){t.dispatchQ.push(t.dispatchWithCallback.bind(t,e,n))}))]}))}))},e.prototype.process=function(e){return r(this,void 0,void 0,(function(){var t,n,i;return o(this,(function(r){switch(r.label){case 0:return r.trys.push([0,2,,3]),this.config.optOut?[2,N(e,0,"Event skipped due to optOut config")]:[4,this.timeline.push(e)];case 1:return 200===(i=r.sent()).code?this.config.loggerProvider.log(i.message):100===i.code?this.config.loggerProvider.warn(i.message):this.config.loggerProvider.error(i.message),[2,i];case 2:return t=r.sent(),n=String(t),this.config.loggerProvider.error(n),[2,i=N(e,0,n)];case 3:return[2]}}))}))},e.prototype.setOptOut=function(e){this.config?this.config.optOut=Boolean(e):this.q.push(this.setOptOut.bind(this,Boolean(e)))},e.prototype.flush=function(){return $(this.timeline.flush())},e}(),z=function(){function e(){this.productId="",this.quantity=1,this.price=0}return e.prototype.setProductId=function(e){return this.productId=e,this},e.prototype.setQuantity=function(e){return e>0&&(this.quantity=e),this},e.prototype.setPrice=function(e){return this.price=e,this},e.prototype.setRevenueType=function(e){return this.revenueType=e,this},e.prototype.setRevenue=function(e){return this.revenue=e,this},e.prototype.setEventProperties=function(e){return V(e)&&(this.properties=e),this},e.prototype.getEventProperties=function(){var e=this.properties?n({},this.properties):{};return e[d.REVENUE_PRODUCT_ID]=this.productId,e[d.REVENUE_QUANTITY]=this.quantity,e[d.REVENUE_PRICE]=this.price,e[d.REVENUE_TYPE]=this.revenueType,e[d.REVENUE]=this.revenue,e},e}(),W="Amplitude Logger ",J=function(){function e(){this.logLevel=f.None}return e.prototype.disable=function(){this.logLevel=f.None},e.prototype.enable=function(e){void 0===e&&(e=f.Warn),this.logLevel=e},e.prototype.log=function(){for(var e=[],t=0;t0&&Promise.all(n.map((function(e){return i.execute(e)}))).catch(),[2,Promise.resolve(void 0)]}}))}))},e.prototype.execute=function(e){var t=this;return new Promise((function(n){var i={event:e,attempts:0,callback:function(e){return n(e)},timeout:0};t.addToQueue(i)}))},e.prototype.addToQueue=function(){for(var e=this,t=[],n=0;n0&&t.schedule(e)}))}),e))},e.prototype.flush=function(e){return void 0===e&&(e=!1),r(this,void 0,void 0,(function(){var t,n,i,r=this;return o(this,(function(o){switch(o.label){case 0:return t=[],n=[],this.queue.forEach((function(e){return 0===e.timeout?t.push(e):n.push(e)})),this.queue=n,this.scheduled&&(clearTimeout(this.scheduled),this.scheduled=null),s=t,u=this.config.flushQueueSize,a=Math.max(u,1),i=s.reduce((function(e,t,n){var i=Math.floor(n/a);return e[i]||(e[i]=[]),e[i].push(t),e}),[]),[4,Promise.all(i.map((function(t){return r.send(t,e)})))];case 1:return o.sent(),[2]}var s,u,a}))}))},e.prototype.send=function(e,t){return void 0===t&&(t=!0),r(this,void 0,void 0,(function(){var n,r,s,u,a;return o(this,(function(o){switch(o.label){case 0:if(!this.config.apiKey)return[2,this.fulfillRequest(e,400,"Event rejected due to missing API key")];n={api_key:this.config.apiKey,events:e.map((function(e){var t=e.event;return t.extra,i(t,["extra"])})),options:{min_id_length:this.config.minIdLength},client_upload_time:(new Date).toISOString()},o.label=1;case 1:return o.trys.push([1,3,,4]),r=se(this.config.serverUrl,this.config.serverZone,this.config.useBatch).serverUrl,[4,this.config.transportProvider.send(r,n)];case 2:return null===(s=o.sent())?(this.config.diagnosticProvider.track(e.length,0,G),this.fulfillRequest(e,0,"Unexpected error occurred"),[2]):t?(this.handleResponse(s,e),[3,4]):("body"in s?this.fulfillRequest(e,s.statusCode,"".concat(s.status,": ").concat(ue(s))):this.fulfillRequest(e,s.statusCode,s.status),this.processResponseDiagnostics(s,e),[2]);case 3:return u=o.sent(),a=(c=u)instanceof Error?c.message:String(c),this.config.loggerProvider.error(a),this.fulfillRequest(e,0,a),this.config.diagnosticProvider.track(e.length,0,G),[3,4];case 4:return[2]}var c}))}))},e.prototype.processResponseDiagnostics=function(e,t){switch(e.status){case v.Invalid:e.body.missingField&&!e.body.error.startsWith(q)?this.config.diagnosticProvider.track(t.length,400,H):this.config.diagnosticProvider.track(t.length,400,X);break;case v.PayloadTooLarge:this.config.diagnosticProvider.track(t.length,413,ee);break;case v.RateLimit:this.config.diagnosticProvider.track(t.length,429,te);break;default:this.config.diagnosticProvider.track(t.length,0,G)}},e.prototype.handleResponse=function(e,t){var n=e.status;switch(n){case v.Success:this.handleSuccessResponse(e,t);break;case v.Invalid:this.handleInvalidResponse(e,t);break;case v.PayloadTooLarge:this.handlePayloadTooLargeResponse(e,t);break;case v.RateLimit:this.handleRateLimitResponse(e,t);break;default:this.config.loggerProvider.warn("{code: 0, error: \"Status '".concat(n,"' provided for ").concat(t.length,' events"}')),this.handleOtherResponse(t)}},e.prototype.handleSuccessResponse=function(e,t){this.fulfillRequest(t,e.statusCode,"Event tracked successfully")},e.prototype.handleInvalidResponse=function(e,t){var n=this;if(e.body.missingField&&!e.body.error.startsWith(q)&&this.config.diagnosticProvider.track(t.length,400,H),e.body.missingField||e.body.error.startsWith(q))this.fulfillRequest(t,e.statusCode,e.body.error);else{var i=a(a(a(a([],u(Object.values(e.body.eventsWithInvalidFields)),!1),u(Object.values(e.body.eventsWithMissingFields)),!1),u(Object.values(e.body.eventsWithInvalidIdLengths)),!1),u(e.body.silencedEvents),!1).flat(),r=new Set(i);r.size&&this.config.diagnosticProvider.track(r.size,400,X);var o=t.filter((function(t,i){if(!r.has(i))return!0;n.fulfillRequest([t],e.statusCode,e.body.error)}));o.length>0&&this.config.loggerProvider.warn(ue(e)),this.addToQueue.apply(this,a([],u(o),!1))}},e.prototype.handlePayloadTooLargeResponse=function(e,t){if(1===t.length)return this.config.diagnosticProvider.track(t.length,413,ee),void this.fulfillRequest(t,e.statusCode,e.body.error);this.config.loggerProvider.warn(ue(e)),this.config.flushQueueSize/=2,this.addToQueue.apply(this,a([],u(t),!1))},e.prototype.handleRateLimitResponse=function(e,t){var n=this,i=Object.keys(e.body.exceededDailyQuotaUsers),r=Object.keys(e.body.exceededDailyQuotaDevices),o=e.body.throttledEvents,s=new Set(i),c=new Set(r),d=new Set(o),l=[],f=t.filter((function(t,i){return t.event.user_id&&s.has(t.event.user_id)||t.event.device_id&&c.has(t.event.device_id)?(n.fulfillRequest([t],e.statusCode,e.body.error),void l.push(t)):(d.has(i)&&(t.timeout=n.throttleTimeout),!0)}));l.length>0&&this.config.diagnosticProvider.track(l.length,429,te),f.length>0&&this.config.loggerProvider.warn(ue(e)),this.addToQueue.apply(this,a([],u(f),!1))},e.prototype.handleOtherResponse=function(e){var t=this;this.addToQueue.apply(this,a([],u(e.map((function(e){return e.timeout=e.attempts*t.retryTimeout,e}))),!1))},e.prototype.fulfillRequest=function(e,t,n){this.saveEvents(),e.forEach((function(e){return e.callback(N(e.event,t,n))}))},e.prototype.saveEvents=function(){if(this.config.storageProvider){var e=Array.from(this.queue.map((function(e){return e.event})));this.config.storageProvider.set(this.storageKey,e)}},e}(),ce=function(e){return void 0===e&&(e=0),((new Error).stack||"").split("\n").slice(2+e).map((function(e){return e.trim()}))},de=function(e){return function(){var t=n({},e.config);return{logger:t.loggerProvider,logLevel:t.logLevel}}},le=function(e,t){var n,i;t=(t=t.replace(/\[(\w+)\]/g,".$1")).replace(/^\./,"");try{for(var r=s(t.split(".")),o=r.next();!o.done;o=r.next()){var u=o.value;if(!(u in e))return;e=e[u]}}catch(e){n={error:e}}finally{try{o&&!o.done&&(i=r.return)&&i.call(r)}finally{if(n)throw n.error}}return e},fe=function(e,t){return function(){var n,i,r={};try{for(var o=s(t),u=o.next();!u.done;u=o.next()){var a=u.value;r[a]=le(e,a)}}catch(e){n={error:e}}finally{try{u&&!u.done&&(i=o.return)&&i.call(o)}finally{if(n)throw n.error}}return r}},pe=function(e,t,n,i,r){return void 0===r&&(r=null),function(){for(var o=[],s=0;s=200&&e<300?v.Success:429===e?v.RateLimit:413===e?v.PayloadTooLarge:408===e?v.Timeout:e>=400&&e<500?v.Invalid:e>=500?v.Failed:v.Unknown},e}(),ge=function(e,t,n){return void 0===t&&(t=""),void 0===n&&(n=10),[j,t,e.substring(0,n)].filter(Boolean).join("_")},ye=function(e,t){return void 0===t&&(t=Date.now()),Date.now()-t>e},me=function(){function e(e){this.options=n({},e)}return e.prototype.isEnabled=function(){return r(this,void 0,void 0,(function(){var t,n;return o(this,(function(i){switch(i.label){case 0:if(!h())return[2,!1];e.testValue=String(Date.now()),t=new e(this.options),n="AMP_TEST",i.label=1;case 1:return i.trys.push([1,4,5,7]),[4,t.set(n,e.testValue)];case 2:return i.sent(),[4,t.get(n)];case 3:return[2,i.sent()===e.testValue];case 4:return i.sent(),[2,!1];case 5:return[4,t.remove(n)];case 6:return i.sent(),[7];case 7:return[2]}}))}))},e.prototype.get=function(e){return r(this,void 0,void 0,(function(){var t;return o(this,(function(n){switch(n.label){case 0:return[4,this.getRaw(e)];case 1:if(!(t=n.sent()))return[2,void 0];try{try{t=decodeURIComponent(atob(t))}catch(e){}return[2,JSON.parse(t)]}catch(e){return[2,void 0]}return[2]}}))}))},e.prototype.getRaw=function(e){var t,n;return r(this,void 0,void 0,(function(){var i,r,s;return o(this,(function(o){return i=h(),r=null!==(n=null===(t=null==i?void 0:i.document)||void 0===t?void 0:t.cookie.split("; "))&&void 0!==n?n:[],(s=r.find((function(t){return 0===t.indexOf(e+"=")})))?[2,s.substring(e.length+1)]:[2,void 0]}))}))},e.prototype.set=function(e,t){var n;return r(this,void 0,void 0,(function(){var i,r,s,u,a,c;return o(this,(function(o){try{i=null!==(n=this.options.expirationDays)&&void 0!==n?n:0,s=void 0,(r=null!==t?i:-1)&&((u=new Date).setTime(u.getTime()+24*r*60*60*1e3),s=u),a="".concat(e,"=").concat(btoa(encodeURIComponent(JSON.stringify(t)))),s&&(a+="; expires=".concat(s.toUTCString())),a+="; path=/",this.options.domain&&(a+="; domain=".concat(this.options.domain)),this.options.secure&&(a+="; Secure"),this.options.sameSite&&(a+="; SameSite=".concat(this.options.sameSite)),(c=h())&&(c.document.cookie=a)}catch(e){}return[2]}))}))},e.prototype.remove=function(e){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,this.set(e,null)];case 1:return t.sent(),[2]}}))}))},e.prototype.reset=function(){return r(this,void 0,void 0,(function(){return o(this,(function(e){return[2]}))}))},e}(),be=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var n,i;return o(this,(function(r){switch(r.label){case 0:if("undefined"==typeof fetch)throw new Error("FetchTransport is not supported");return n={headers:{"Content-Type":"application/json",Accept:"*/*"},body:JSON.stringify(t),method:"POST"},[4,fetch(e,n)];case 1:return[4,r.sent().json()];case 2:return i=r.sent(),[2,this.buildResponse(i)]}}))}))},n}(he),_e=function(){function e(){}return e.prototype.getApplicationContext=function(){return{versionName:this.versionName,language:Ie(),platform:"Web",os:void 0,deviceModel:void 0}},e}(),Ie=function(){return"undefined"!=typeof navigator&&(navigator.languages&&navigator.languages[0]||navigator.language)||""},we=function(){function e(){this.queue=[]}return e.prototype.logEvent=function(e){this.receiver?this.receiver(e):this.queue.length<512&&this.queue.push(e)},e.prototype.setEventReceiver=function(e){this.receiver=e,this.queue.length>0&&(this.queue.forEach((function(t){e(t)})),this.queue=[])},e}(),Se=function(){return Se=Object.assign||function(e){for(var t,n=1,i=arguments.length;nMe?(r=n.length-Me,[4,e.prototype.set.call(this,t,n.slice(0,Me))]):[3,2];case 1:return o.sent(),null===(i=this.loggerProvider)||void 0===i||i.error("Failed to save ".concat(r," events because the queue length exceeded ").concat(Me,".")),[3,4];case 2:return[4,e.prototype.set.call(this,t,n)];case 3:o.sent(),o.label=4;case 4:return[2]}}))}))},n}(Ce),Fe=function(e){function n(){var t;return e.call(this,null===(t=h())||void 0===t?void 0:t.sessionStorage)||this}return t(n,e),n}(Ce),Ke=function(e){function n(){var t=null!==e&&e.apply(this,arguments)||this;return t.state={done:4},t}return t(n,e),n.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var n=this;return o(this,(function(i){return[2,new Promise((function(i,r){"undefined"==typeof XMLHttpRequest&&r(new Error("XHRTransport is not supported."));var o=new XMLHttpRequest;o.open("POST",e,!0),o.onreadystatechange=function(){if(o.readyState===n.state.done)try{var e=o.responseText,t=JSON.parse(e),s=n.buildResponse(t);i(s)}catch(e){r(e)}},o.setRequestHeader("Content-Type","application/json"),o.setRequestHeader("Accept","*/*"),o.send(JSON.stringify(t))}))]}))}))},n}(he),Qe=function(e){function n(){return null!==e&&e.apply(this,arguments)||this}return t(n,e),n.prototype.send=function(e,t){return r(this,void 0,void 0,(function(){var n=this;return o(this,(function(i){return[2,new Promise((function(i,r){var o=h();if(!(null==o?void 0:o.navigator.sendBeacon))throw new Error("SendBeaconTransport is not supported");try{var s=JSON.stringify(t);return i(o.navigator.sendBeacon(e,JSON.stringify(t))?n.buildResponse({code:200,events_ingested:t.events.length,payload_size_bytes:s.length,server_upload_time:Date.now()}):n.buildResponse({code:500}))}catch(e){r(e)}}))]}))}))},n}(he),$e=function(e,t,n){return void 0===n&&(n=!0),r(void 0,void 0,void 0,(function(){var i,r,s,a,c,d,l,f,p;return o(this,(function(o){switch(o.label){case 0:return i=function(e){return"".concat(j.toLowerCase(),"_").concat(e.substring(0,6))}(e),[4,t.getRaw(i)];case 1:return(r=o.sent())?n?[4,t.remove(i)]:[3,3]:[2,{optOut:!1}];case 2:o.sent(),o.label=3;case 3:return s=u(r.split("."),6),a=s[0],c=s[1],d=s[2],l=s[3],f=s[4],p=s[5],[2,{deviceId:a,userId:ze(c),sessionId:Be(l),lastEventId:Be(p),lastEventTime:Be(f),optOut:Boolean(d)}]}}))}))},Be=function(e){var t=parseInt(e,32);if(!isNaN(t))return t},ze=function(e){if(atob&&escape&&e)try{return decodeURIComponent(escape(atob(e)))}catch(e){return}},We="[Amplitude]",Je="".concat(We," Form Started"),Ze="".concat(We," Form Submitted"),Ye="".concat(We," File Downloaded"),Ge="session_start",He="session_end",Xe="".concat(We," File Extension"),et="".concat(We," File Name"),tt="".concat(We," Link ID"),nt="".concat(We," Link Text"),it="".concat(We," Link URL"),rt="".concat(We," Form ID"),ot="".concat(We," Form Name"),st="".concat(We," Form Destination"),ut="cookie",at=function(e){function n(){var t=null!==e&&e.apply(this,arguments)||this;return t.isDisabled=!1,t}return t(n,e),n.prototype.flush=function(){return r(this,void 0,void 0,(function(){return o(this,(function(t){switch(t.label){case 0:return[4,fetch(this.serverUrl,this.requestPayloadBuilder(this.queue))];case 1:return t.sent(),[4,e.prototype.flush.call(this)];case 2:return t.sent(),[2]}}))}))},n}(ne),ct=function(e){function n(t,n,i,r,o,s,u,a,c,d,l,p,v,h,g,y,m,b,_,I,w,S,E,T,P,k,O,R,x,U){void 0===i&&(i=new ve),void 0===r&&(r={domain:"",expiration:365,sameSite:"Lax",secure:!1,upgrade:!0}),void 0===u&&(u=1e3),void 0===a&&(a=5),void 0===c&&(c=30),void 0===d&&(d=ut),void 0===g&&(g=new J),void 0===y&&(y=f.Warn),void 0===b&&(b=!1),void 0===w&&(w=""),void 0===S&&(S="US"),void 0===T&&(T=18e5),void 0===P&&(P=new Ve({loggerProvider:g})),void 0===k&&(k={ipAddress:!0,language:!0,platform:!0}),void 0===O&&(O="fetch"),void 0===R&&(R=!1),void 0===x&&(x=new at);var q=e.call(this,{apiKey:t,storageProvider:P,transportProvider:ft(O),diagnosticProvider:x})||this;return q.apiKey=t,q.appVersion=n,q.cookieOptions=r,q.defaultTracking=o,q.flushIntervalMillis=u,q.flushMaxRetries=a,q.flushQueueSize=c,q.identityStorage=d,q.ingestionMetadata=l,q.instanceName=p,q.loggerProvider=g,q.logLevel=y,q.minIdLength=m,q.partnerId=_,q.plan=I,q.serverUrl=w,q.serverZone=S,q.sessionTimeout=T,q.storageProvider=P,q.trackingOptions=k,q.transport=O,q.useBatch=R,q.diagnosticProvider=x,q._optOut=!1,q._cookieStorage=i,q.deviceId=s,q.lastEventId=v,q.lastEventTime=h,q.optOut=b,q.sessionId=E,q.userId=U,q.loggerProvider.enable(q.logLevel),q}return t(n,e),Object.defineProperty(n.prototype,"cookieStorage",{get:function(){return this._cookieStorage},set:function(e){this._cookieStorage!==e&&(this._cookieStorage=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"deviceId",{get:function(){return this._deviceId},set:function(e){this._deviceId!==e&&(this._deviceId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"userId",{get:function(){return this._userId},set:function(e){this._userId!==e&&(this._userId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"sessionId",{get:function(){return this._sessionId},set:function(e){this._sessionId!==e&&(this._sessionId=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"optOut",{get:function(){return this._optOut},set:function(e){this._optOut!==e&&(this._optOut=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"lastEventTime",{get:function(){return this._lastEventTime},set:function(e){this._lastEventTime!==e&&(this._lastEventTime=e,this.updateStorage())},enumerable:!1,configurable:!0}),Object.defineProperty(n.prototype,"lastEventId",{get:function(){return this._lastEventId},set:function(e){this._lastEventId!==e&&(this._lastEventId=e,this.updateStorage())},enumerable:!1,configurable:!0}),n.prototype.updateStorage=function(){var e={deviceId:this._deviceId,userId:this._userId,sessionId:this._sessionId,optOut:this._optOut,lastEventTime:this._lastEventTime,lastEventId:this._lastEventId};this.cookieStorage.set(ge(this.apiKey),e)},n}(re),dt=function(e,t,i){return void 0===t&&(t={}),r(void 0,void 0,void 0,(function(){var r,s,u,a,c,d,l,f,p,v,h,y,m,b,_,I,w,S,E,T,P,k,O,R,x,U,q,D,N,A,j,C,M,V,F,K,Q,$,B;return o(this,(function(o){switch(o.label){case 0:return r=t.identityStorage||ut,I={},r===ut?[3,1]:(u="",[3,5]);case 1:return null===(S=null===(w=t.cookieOptions)||void 0===w?void 0:w.domain)||void 0===S?[3,2]:(a=S,[3,4]);case 2:return[4,pt()];case 3:a=o.sent(),o.label=4;case 4:u=a,o.label=5;case 5:return s=n.apply(void 0,[(I.domain=u,I.expiration=365,I.sameSite="Lax",I.secure=!1,I.upgrade=!0,I),t.cookieOptions]),c=lt(t.identityStorage,s),[4,$e(e,c,null===(T=null===(E=t.cookieOptions)||void 0===E?void 0:E.upgrade)||void 0===T||T)];case 6:return d=o.sent(),[4,c.get(ge(e))];case 7:return l=o.sent(),f=g(),p=null!==(R=null!==(O=null!==(k=null!==(P=t.deviceId)&&void 0!==P?P:f.deviceId)&&void 0!==k?k:null==l?void 0:l.deviceId)&&void 0!==O?O:d.deviceId)&&void 0!==R?R:L(),v=null!==(x=null==l?void 0:l.lastEventId)&&void 0!==x?x:d.lastEventId,h=null!==(U=null==l?void 0:l.lastEventTime)&&void 0!==U?U:d.lastEventTime,y=null!==(D=null!==(q=t.optOut)&&void 0!==q?q:null==l?void 0:l.optOut)&&void 0!==D?D:d.optOut,m=null!==(N=null==l?void 0:l.sessionId)&&void 0!==N?N:d.sessionId,b=null!==(j=null!==(A=t.userId)&&void 0!==A?A:null==l?void 0:l.userId)&&void 0!==j?j:d.userId,i.previousSessionDeviceId=null!==(C=null==l?void 0:l.deviceId)&&void 0!==C?C:d.deviceId,i.previousSessionUserId=null!==(M=null==l?void 0:l.userId)&&void 0!==M?M:d.userId,_={ipAddress:null===(F=null===(V=t.trackingOptions)||void 0===V?void 0:V.ipAddress)||void 0===F||F,language:null===(Q=null===(K=t.trackingOptions)||void 0===K?void 0:K.language)||void 0===Q||Q,platform:null===(B=null===($=t.trackingOptions)||void 0===$?void 0:$.platform)||void 0===B||B},[2,new ct(e,t.appVersion,c,s,t.defaultTracking,p,t.flushIntervalMillis,t.flushMaxRetries,t.flushQueueSize,r,t.ingestionMetadata,t.instanceName,v,h,t.loggerProvider,t.logLevel,t.minIdLength,y,t.partnerId,t.plan,t.serverUrl,t.serverZone,m,t.sessionTimeout,t.storageProvider,_,t.transport,t.useBatch,t.diagnosticProvider,b)]}}))}))},lt=function(e,t){switch(void 0===e&&(e=ut),void 0===t&&(t={}),e){case"localStorage":return new Ve;case"sessionStorage":return new Fe;case"none":return new ve;default:return new me(n(n({},t),{expirationDays:t.expiration}))}},ft=function(e){return"xhr"===e?new Ke:"beacon"===e?new Qe:new be},pt=function(e){return r(void 0,void 0,void 0,(function(){var t,n,i,r,s,u,a;return o(this,(function(o){switch(o.label){case 0:return[4,(new me).isEnabled()];case 1:if(!o.sent()||!e&&"undefined"==typeof location)return[2,""];for(t=null!=e?e:location.hostname,n=t.split("."),i=[],r="AMP_TLDTEST",s=n.length-2;s>=0;--s)i.push(n.slice(s).join("."));s=0,o.label=2;case 2:return s2?(r=t,o=i):"string"==typeof t?(r=t,o=void 0):(r=null==t?void 0:t.userId,o=t),$(this._init(n(n({},o),{userId:r,apiKey:e})))},i.prototype._init=function(t){var i,s;return r(this,void 0,void 0,(function(){var r,u,a,c,d=this;return o(this,(function(o){switch(o.label){case 0:return this.initializing?[2]:(this.initializing=!0,[4,dt(t.apiKey,t,this)]);case 1:return r=o.sent(),[4,e.prototype._init.call(this,r)];case 2:return o.sent(),this.setSessionId(null!==(s=null!==(i=t.sessionId)&&void 0!==i?i:this.config.sessionId)&&void 0!==s?s:Date.now()),(u=Oe(t.instanceName)).identityStore.setIdentity({userId:this.config.userId,deviceId:this.config.deviceId}),[4,this.add(new ae).promise];case 3:return o.sent(),[4,this.add(new je).promise];case 4:return o.sent(),[4,this.add(new Re).promise];case 5:return o.sent(),f=this.config,Pt||void 0!==f.defaultTracking||(f.loggerProvider.warn("`options.defaultTracking` is set to undefined. This implicitly configures your Amplitude instance to track Page Views, Sessions, File Downloads, and Form Interactions. You can suppress this warning by explicitly setting a value to `options.defaultTracking`. The value must either be a boolean, to enable and disable all default events, or an object, for advanced configuration. For example:\n\namplitude.init(, {\n defaultTracking: true,\n});\n\nVisit https://www.docs.developers.amplitude.com/data/sdks/browser-2/#tracking-default-events for more details."),Pt=!0),l=this.config.defaultTracking,Ue(l,"fileDownloads")?[4,this.add(Tt()).promise]:[3,7];case 6:o.sent(),o.label=7;case 7:return function(e){return Ue(e,"formInteractions")}(this.config.defaultTracking)?[4,this.add(St()).promise]:[3,9];case 8:o.sent(),o.label=9;case 9:return qe(this.config.defaultTracking)?(a=function(e){return qe(e.defaultTracking)&&e.defaultTracking&&"object"==typeof e.defaultTracking&&e.defaultTracking.attribution&&"object"==typeof e.defaultTracking.attribution?n({},e.defaultTracking.attribution):{}}(this.config),c=yt(a),[4,this.add(c).promise]):[3,11];case 10:o.sent(),o.label=11;case 11:return[4,this.add(bt(De(this.config))).promise];case 12:return o.sent(),this.initializing=!1,[4,this.runQueuedFunctions("dispatchQ")];case 13:return o.sent(),u.eventBridge.setEventReceiver((function(e){d.track(e.eventType,e.eventProperties)})),[2]}var l,f}))}))},i.prototype.getUserId=function(){var e;return null===(e=this.config)||void 0===e?void 0:e.userId},i.prototype.setUserId=function(e){this.config?e===this.config.userId&&void 0!==e||(this.config.userId=e,function(e,t){Oe(t).identityStore.editIdentity().setUserId(e).commit()}(e,this.config.instanceName)):this.q.push(this.setUserId.bind(this,e))},i.prototype.getDeviceId=function(){var e;return null===(e=this.config)||void 0===e?void 0:e.deviceId},i.prototype.setDeviceId=function(e){this.config?(this.config.deviceId=e,function(e,t){Oe(t).identityStore.editIdentity().setDeviceId(e).commit()}(e,this.config.instanceName)):this.q.push(this.setDeviceId.bind(this,e))},i.prototype.reset=function(){this.setDeviceId(L()),this.setUserId(void 0)},i.prototype.getSessionId=function(){var e;return null===(e=this.config)||void 0===e?void 0:e.sessionId},i.prototype.setSessionId=function(e){var t;if(this.config){if(e!==this.config.sessionId){var n,i=this.getSessionId(),r=this.config.lastEventTime,o=null!==(t=this.config.lastEventId)&&void 0!==t?t:-1;this.config.sessionId=e,this.config.lastEventTime=void 0,n=this.config.defaultTracking,Ue(n,"sessions")&&(i&&r&&this.track(He,void 0,{device_id:this.previousSessionDeviceId,event_id:++o,session_id:i,time:r+1,user_id:this.previousSessionUserId}),this.config.lastEventTime=this.config.sessionId,this.track(Ge,void 0,{event_id:++o,session_id:this.config.sessionId,time:this.config.lastEventTime})),this.previousSessionDeviceId=this.config.deviceId,this.previousSessionUserId=this.config.userId}}else this.q.push(this.setSessionId.bind(this,e))},i.prototype.extendSession=function(){this.config?this.config.lastEventTime=Date.now():this.q.push(this.extendSession.bind(this))},i.prototype.setTransport=function(e){this.config?this.config.transportProvider=ft(e):this.q.push(this.setTransport.bind(this,e))},i.prototype.identify=function(t,n){if(Ae(t)){var i=t._q;t._q=[],t=Le(new K,i)}return(null==n?void 0:n.user_id)&&this.setUserId(n.user_id),(null==n?void 0:n.device_id)&&this.setDeviceId(n.device_id),e.prototype.identify.call(this,t,n)},i.prototype.groupIdentify=function(t,n,i,r){if(Ae(i)){var o=i._q;i._q=[],i=Le(new K,o)}return e.prototype.groupIdentify.call(this,t,n,i,r)},i.prototype.revenue=function(t,n){if(Ae(t)){var i=t._q;t._q=[],t=Le(new z,i)}return e.prototype.revenue.call(this,t,n)},i.prototype.process=function(t){return r(this,void 0,void 0,(function(){var n,i;return o(this,(function(r){return n=Date.now(),i=ye(this.config.sessionTimeout,this.config.lastEventTime),t.event_type===Ge||t.event_type===He||t.session_id&&t.session_id!==this.getSessionId()||!i||this.setSessionId(n),[2,e.prototype.process.call(this,t)]}))}))},i}(B),Ot=function(){var e=new kt;return{init:pe(e.init.bind(e),"init",de(e),fe(e,["config"])),add:pe(e.add.bind(e),"add",de(e),fe(e,["config.apiKey","timeline.plugins"])),remove:pe(e.remove.bind(e),"remove",de(e),fe(e,["config.apiKey","timeline.plugins"])),track:pe(e.track.bind(e),"track",de(e),fe(e,["config.apiKey","timeline.queue.length"])),logEvent:pe(e.logEvent.bind(e),"logEvent",de(e),fe(e,["config.apiKey","timeline.queue.length"])),identify:pe(e.identify.bind(e),"identify",de(e),fe(e,["config.apiKey","timeline.queue.length"])),groupIdentify:pe(e.groupIdentify.bind(e),"groupIdentify",de(e),fe(e,["config.apiKey","timeline.queue.length"])),setGroup:pe(e.setGroup.bind(e),"setGroup",de(e),fe(e,["config.apiKey","timeline.queue.length"])),revenue:pe(e.revenue.bind(e),"revenue",de(e),fe(e,["config.apiKey","timeline.queue.length"])),flush:pe(e.flush.bind(e),"flush",de(e),fe(e,["config.apiKey","timeline.queue.length"])),getUserId:pe(e.getUserId.bind(e),"getUserId",de(e),fe(e,["config","config.userId"])),setUserId:pe(e.setUserId.bind(e),"setUserId",de(e),fe(e,["config","config.userId"])),getDeviceId:pe(e.getDeviceId.bind(e),"getDeviceId",de(e),fe(e,["config","config.deviceId"])),setDeviceId:pe(e.setDeviceId.bind(e),"setDeviceId",de(e),fe(e,["config","config.deviceId"])),reset:pe(e.reset.bind(e),"reset",de(e),fe(e,["config","config.userId","config.deviceId"])),getSessionId:pe(e.getSessionId.bind(e),"getSessionId",de(e),fe(e,["config"])),setSessionId:pe(e.setSessionId.bind(e),"setSessionId",de(e),fe(e,["config"])),extendSession:pe(e.extendSession.bind(e),"extendSession",de(e),fe(e,["config"])),setOptOut:pe(e.setOptOut.bind(e),"setOptOut",de(e),fe(e,["config"])),setTransport:pe(e.setTransport.bind(e),"setTransport",de(e),fe(e,["config"]))}},Rt=Ot(),xt=Rt.add,Ut=Rt.extendSession,qt=Rt.flush,Dt=Rt.getDeviceId,Nt=Rt.getSessionId,Lt=Rt.getUserId,At=Rt.groupIdentify,jt=Rt.identify,Ct=Rt.init,Mt=Rt.logEvent,Vt=Rt.remove,Ft=Rt.reset,Kt=Rt.revenue,Qt=Rt.setDeviceId,$t=Rt.setGroup,Bt=Rt.setOptOut,zt=Rt.setSessionId,Wt=Rt.setTransport,Jt=Rt.setUserId,Zt=Rt.track,Yt=Object.freeze({__proto__:null,add:xt,extendSession:Ut,flush:qt,getDeviceId:Dt,getSessionId:Nt,getUserId:Lt,groupIdentify:At,identify:jt,init:Ct,logEvent:Mt,remove:Vt,reset:Ft,revenue:Kt,setDeviceId:Qt,setGroup:$t,setOptOut:Bt,setSessionId:zt,setTransport:Wt,setUserId:Jt,track:Zt,Types:D,createInstance:Ot,runQueuedFunctions:Ne,Revenue:z,Identify:K});!function(){var e=h();if(e){var t=function(e){var t=Ot(),n=h();return n&&n.amplitude&&n.amplitude._iq&&e&&(n.amplitude._iq[e]=t),t};if(e.amplitude=Object.assign(e.amplitude||{},Yt,{createInstance:t}),e.amplitude.invoked){var n=e.amplitude._q;e.amplitude._q=[],Ne(Yt,n);for(var i=Object.keys(e.amplitude._iq)||[],r=0;r