Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/analytics-browser/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class BrowserConfig extends Config implements IBrowserConfig {
public partnerId?: string,
public plan?: Plan,
public serverUrl: string = '',
public delayedEventsServerUrl?: string,
delayedEventsServerUrl?: string,
public serverZone: ServerZoneType = DEFAULT_SERVER_ZONE,
sessionId?: number,
deferredSessionId?: number,
Expand All @@ -135,7 +135,13 @@ export class BrowserConfig extends Config implements IBrowserConfig {
public enableRequestBodyCompression: boolean = false,
public customEnrichment?: boolean | CustomEnrichmentOptions,
) {
super({ apiKey, storageProvider, transportProvider: createTransport(transport) });
super({
apiKey,
storageProvider,
transportProvider: createTransport(transport),
delayedEventsServerUrl,
serverZone,
});
this._cookieStorage = cookieStorage;
this.deviceId = deviceId;
this.lastEventId = lastEventId;
Expand Down
18 changes: 17 additions & 1 deletion packages/analytics-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ export class Config implements IConfig {
this.offline = options.offline !== undefined ? options.offline : defaultConfig.offline;
this.optOut = options.optOut ?? defaultConfig.optOut;
this.serverUrl = options.serverUrl;
this.delayedEventsServerUrl = options.delayedEventsServerUrl;
this.serverZone = options.serverZone || defaultConfig.serverZone;
this.storageProvider = options.storageProvider;
this.transportProvider = options.transportProvider;
Expand All @@ -83,6 +82,7 @@ export class Config implements IConfig {
const serverConfig = createServerConfig(options.serverUrl, options.serverZone, options.useBatch);
this.serverZone = serverConfig.serverZone;
this.serverUrl = serverConfig.serverUrl;
this.delayedEventsServerUrl = getDelayedEventsServerUrl(options.delayedEventsServerUrl, this.serverZone);
}
}

Expand All @@ -108,6 +108,22 @@ export const createServerConfig = (
};
};

export const getDelayedEventsServerUrl = (
delayedEventsServerUrl: string | undefined,
serverZone: ServerZoneType = getDefaultConfig().serverZone,
) => {
if (delayedEventsServerUrl) {
return delayedEventsServerUrl;
}
switch (serverZone) {
case 'EU':
return 'https://delayed-events.prod.eu-central-1.amplitude.com/2/httpapi/delayed';
case 'US':
default:
return 'https://delayed-events.prod.us-west-2.amplitude.com/2/httpapi/delayed';
}
};

export class RequestMetadata implements IRequestMetadata {
sdk: {
metrics: {
Expand Down
13 changes: 11 additions & 2 deletions packages/analytics-core/src/heartbeat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CoreClient } from './types/client/core-client';
import { BaseEvent, Delay } from './types/event/base-event';
import { Result } from './types/result';
import { UUID } from './utils/uuid';
import { getGlobalScope } from './global-scope';

type DelayedEvent = BaseEvent & {
delay: Delay;
Expand All @@ -15,7 +16,7 @@ export class Heartbeat {
private delayId: string;
private interval: NodeJS.Timeout | null = null;
private resetPromise: Promise<Result[]> | null = null;

private global: ReturnType<typeof getGlobalScope> | undefined;
constructor(
private client: CoreClient,
private pulse: number,
Expand All @@ -24,6 +25,15 @@ export class Heartbeat {
) {
this.events = new Map<string, DelayedEvent>();
this.delayId = UUID();
this.global = getGlobalScope();
if (typeof Window !== 'undefined' && this.global instanceof Window) {
const win = this.global;
win.addEventListener('visibilitychange', () => {
if (win.document.visibilityState === 'hidden') {
void this.resetHeartbeat();
}
});
}
}

private async heartbeat(): Promise<Result[]> {
Expand Down Expand Up @@ -146,7 +156,6 @@ export class Heartbeat {
}

const DEFAULT_HEARTBEAT_INTERVAL = 60_000;
// const DEFAULT_HEARTBEAT_INTERVAL = 2_000; // TODO: DO NOT MERGE THIS
const DEFAULT_HEARTBEAT_DELAY_TIMEOUT = 3_600_000;

type HeartbeatMap = Map<CoreClient, Heartbeat>;
Expand Down
17 changes: 15 additions & 2 deletions packages/analytics-core/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('config', () => {
plan: undefined,
ingestionMetadata: undefined,
serverUrl: 'https://api2.amplitude.com/2/httpapi',
delayedEventsServerUrl: undefined,
delayedEventsServerUrl: 'https://delayed-events.prod.us-west-2.amplitude.com/2/httpapi/delayed',
serverZone: 'US',
storageProvider: defaultConfig.storageProvider,
transportProvider: defaultConfig.transportProvider,
Expand All @@ -55,6 +55,19 @@ describe('config', () => {
expect(config.delayedEventsServerUrl).toBe(delayedEventsServerUrl);
});

test('should default delayedEventsServerUrl for EU', () => {
const defaultConfig = useDefaultConfig();
const config = new Config({
apiKey: API_KEY,
serverZone: 'EU',
storageProvider: defaultConfig.storageProvider,
transportProvider: defaultConfig.transportProvider,
});
expect(config.delayedEventsServerUrl).toBe(
'https://delayed-events.prod.eu-central-1.amplitude.com/2/httpapi/delayed',
);
});

test('should overwrite default config', () => {
const defaultConfig = useDefaultConfig();
const config = new Config({
Expand Down Expand Up @@ -90,7 +103,7 @@ describe('config', () => {
sourceVersion: '2.0.0',
},
serverUrl: 'https://api2.amplitude.com/batch',
delayedEventsServerUrl: undefined,
delayedEventsServerUrl: 'https://delayed-events.prod.us-west-2.amplitude.com/2/httpapi/delayed',
serverZone: 'US',
storageProvider: defaultConfig.storageProvider,
transportProvider: defaultConfig.transportProvider,
Expand Down
70 changes: 70 additions & 0 deletions packages/analytics-core/test/heartbeat.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Heartbeat } from '../src/heartbeat';
import { getHeartbeatInstance } from '../src/';
import { CoreClient } from '../src/types/client/core-client';
import * as GlobalScopeModule from '../src/global-scope';

describe('heartbeat', () => {
let mockClient: CoreClient;
Expand Down Expand Up @@ -330,6 +331,75 @@ describe('heartbeat', () => {
});
});

describe('page visibility', () => {
class FakeWindow {
document = { visibilityState: 'visible' };
private listeners: Array<() => void> = [];
addEventListener = jest.fn((event: string, handler: () => void) => {
if (event === 'visibilitychange') {
this.listeners.push(handler);
}
});
dispatchVisibility(state: string) {
this.document.visibilityState = state;
this.listeners.forEach((handler) => handler());
}
}

let fakeWindow: FakeWindow;
let originalWindow: unknown;

beforeEach(() => {
fakeWindow = new FakeWindow();
originalWindow = (globalThis as { Window?: unknown }).Window;
(globalThis as { Window: unknown }).Window = FakeWindow;
jest.spyOn(GlobalScopeModule, 'getGlobalScope').mockReturnValue(fakeWindow as unknown as typeof globalThis);
heartbeat = new Heartbeat(mockClient, 1000, 1000, mockLoggerProvider);
});

afterEach(() => {
jest.spyOn(GlobalScopeModule, 'getGlobalScope').mockRestore();
if (originalWindow === undefined) {
delete (globalThis as { Window?: unknown }).Window;
} else {
(globalThis as { Window: unknown }).Window = originalWindow;
}
});

test('should subscribe to visibilitychange on Window', () => {
expect(fakeWindow.addEventListener).toHaveBeenCalledWith('visibilitychange', expect.any(Function));
});

test('should reset heartbeat when the page becomes hidden', async () => {
await trackWithTimers({
insert_id: '1',
event_type: 'test',
event_properties: { test: 'test' },
});
expect(trackMock).toHaveBeenCalledTimes(1);
jest.clearAllMocks();

fakeWindow.dispatchVisibility('hidden');
await jest.advanceTimersByTimeAsync(0);

expect(trackMock).toHaveBeenCalledTimes(1);
});

test('should not reset heartbeat when visibility changes to visible', async () => {
await trackWithTimers({
insert_id: '1',
event_type: 'test',
event_properties: { test: 'test' },
});
jest.clearAllMocks();

fakeWindow.dispatchVisibility('visible');
await jest.advanceTimersByTimeAsync(0);

expect(trackMock).not.toHaveBeenCalled();
});
});

describe('.resetHeartbeat', () => {
test('should only reset the heartbeat once if called multiple times', async () => {
const heartbeatMock = jest.spyOn(heartbeat as any, 'heartbeat');
Expand Down
4 changes: 2 additions & 2 deletions packages/analytics-node/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('config', () => {
plan: undefined,
ingestionMetadata: undefined,
serverUrl: 'https://api2.amplitude.com/2/httpapi',
delayedEventsServerUrl: undefined,
delayedEventsServerUrl: 'https://delayed-events.prod.us-west-2.amplitude.com/2/httpapi/delayed',
serverZone: 'US',
storageProvider: undefined,
transportProvider: new Http(),
Expand Down Expand Up @@ -61,7 +61,7 @@ describe('config', () => {
plan: undefined,
ingestionMetadata: undefined,
serverUrl: 'https://api2.amplitude.com/2/httpapi',
delayedEventsServerUrl: undefined,
delayedEventsServerUrl: 'https://delayed-events.prod.us-west-2.amplitude.com/2/httpapi/delayed',
serverZone: 'US',
storageProvider: undefined,
transportProvider: new Http(),
Expand Down
6 changes: 3 additions & 3 deletions packages/analytics-react-native/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('config', () => {
plan: undefined,
ingestionMetadata: undefined,
serverUrl: 'https://api2.amplitude.com/2/httpapi',
delayedEventsServerUrl: undefined,
delayedEventsServerUrl: 'https://delayed-events.prod.us-west-2.amplitude.com/2/httpapi/delayed',
serverZone: 'US',
sessionTimeout: 300000,
trackingOptions: {
Expand Down Expand Up @@ -99,7 +99,7 @@ describe('config', () => {
plan: undefined,
ingestionMetadata: undefined,
serverUrl: 'https://api2.amplitude.com/2/httpapi',
delayedEventsServerUrl: undefined,
delayedEventsServerUrl: 'https://delayed-events.prod.us-west-2.amplitude.com/2/httpapi/delayed',
serverZone: 'US',
sessionTimeout: 300000,
storageProvider: new core.MemoryStorage(),
Expand Down Expand Up @@ -181,7 +181,7 @@ describe('config', () => {
sourceVersion: '2.0.0',
},
serverUrl: 'https://api2.amplitude.com/2/httpapi',
delayedEventsServerUrl: undefined,
delayedEventsServerUrl: 'https://delayed-events.prod.us-west-2.amplitude.com/2/httpapi/delayed',
serverZone: 'US',
_sessionId: -1,
sessionTimeout: 1,
Expand Down
Loading