Skip to content
Open
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
6 changes: 2 additions & 4 deletions .size-limit.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
const limits = [
{
// analytics-browser bundle
// Bumped 65kb → 66kb for shadow DOM support in plugin-autocapture-browser
// (SR-4788). Current actual: ~65.0kb gzipped.
path: './packages/analytics-browser/lib/scripts/amplitude-min.js.gz',
limit: '66kb',
limit: '69kb',
brotli: false,
},
{
Expand All @@ -16,7 +14,7 @@ const limits = [
{
// unified SDK bundle
path: './packages/unified/lib/scripts/amplitude-min.umd.js.gz',
limit: '225kb',
limit: '235kb',
brotli: false,
},
{
Expand Down
22 changes: 22 additions & 0 deletions packages/analytics-browser/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,27 @@ import { AmplitudeBrowser } from './browser-client';
import { VERSION } from './version';
import { getDomain, KNOWN_2LDS } from './attribution/helpers';

export const getDelayedEventsServerUrl = (
serverUrl: string | undefined,
delayedEventsServerUrl: string | undefined,
serverZone: ServerZoneType = DEFAULT_SERVER_ZONE,
) => {
if (serverUrl) {
// serverUrl already includes /2/httpapi; Destination uses the same `${serverUrl}/delayed` fallback.
return `${serverUrl}/delayed`;
}
Comment thread
cursor[bot] marked this conversation as resolved.
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';
}
};

// Exported for testing purposes only. Do not expose to public interface.
export class BrowserConfig extends Config implements IBrowserConfig {
public readonly version = VERSION;
Expand Down Expand Up @@ -142,6 +163,7 @@ export class BrowserConfig extends Config implements IBrowserConfig {
this.fetchRemoteConfig = _fetchRemoteConfig;

this.topLevelDomain = topLevelDomain || getDomain();
this.delayedEventsServerUrl = getDelayedEventsServerUrl(serverUrl, delayedEventsServerUrl, serverZone);
}

get cookieStorage() {
Expand Down
2 changes: 2 additions & 0 deletions packages/analytics-browser/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const DEFAULT_PAGE_VIEW_EVENT = `${DEFAULT_EVENT_PREFIX} Page Viewed`;
export const DEFAULT_FORM_START_EVENT = `${DEFAULT_EVENT_PREFIX} Form Started`;
export const DEFAULT_FORM_SUBMIT_EVENT = `${DEFAULT_EVENT_PREFIX} Form Submitted`;
export const DEFAULT_FILE_DOWNLOAD_EVENT = `${DEFAULT_EVENT_PREFIX} File Downloaded`;
export const DEFAULT_CONTENT_STARTED_EVENT = `${DEFAULT_EVENT_PREFIX} Content Started`;
export const DEFAULT_CONTENT_STOPPED_EVENT = `${DEFAULT_EVENT_PREFIX} Content Stopped`;
export const DEFAULT_SESSION_START_EVENT = 'session_start';
export const DEFAULT_SESSION_END_EVENT = 'session_end';

Expand Down
32 changes: 19 additions & 13 deletions packages/analytics-browser/src/video-capture/video-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import {
BaseEvent,
getHeartbeatInstance,
} from '@amplitude/analytics-core';
import { DEFAULT_CONTENT_STARTED_EVENT, DEFAULT_CONTENT_STOPPED_EVENT } from '../constants';

/** Playback states where a view session is still in progress (e.g. buffering). */
const ACTIVE_PLAYBACK_STATES = new Set<VideoState['playbackState']>(['playing', 'waiting']);

export class VideoCapture {
private videoEl: HTMLVideoElement | null = null;
private videoEl: HTMLMediaElement | null = null;
private heartbeat: ReturnType<typeof getHeartbeatInstance>;
private embeddedVideoPlayer: EmbeddedVideoPlayer | null = null;
private vendor?: VideoVendor;
Expand All @@ -28,12 +29,12 @@ export class VideoCapture {
}

/**
* Specify a video element to capture events from
* Specify a video or audio element to capture events from
*
* @param videoEl - The HTML video element to capture events from.
* @param videoEl - The HTML video or audio element to capture events from.
* @returns The VideoCapture instance.
*/
withVideoElement(videoEl: HTMLVideoElement): VideoCapture {
withVideoElement(videoEl: HTMLMediaElement): VideoCapture {
this.videoEl = videoEl;
return this;
}
Expand Down Expand Up @@ -71,7 +72,7 @@ export class VideoCapture {
}

/**
* Track a "Video Content Started" event every time the video starts playing
* Track a "[Amplitude] Content Started" event every time the video starts playing
* @returns The VideoCapture instance.
*/
captureVideoStarted(): VideoCapture {
Expand All @@ -81,7 +82,7 @@ export class VideoCapture {
const now = new Date().getTime();
const startEvent: BaseEvent = {
insert_id: UUID(),
event_type: 'Video Content Started',
event_type: DEFAULT_CONTENT_STARTED_EVENT,
time: now,
event_properties: {
...nextState.lastEvent,
Expand All @@ -93,7 +94,7 @@ export class VideoCapture {
this.stopEvent = {
...startEvent,
insert_id: UUID(),
event_type: 'Video Content Stopped',
event_type: DEFAULT_CONTENT_STOPPED_EVENT,
time: now + 1,
event_properties: {
...nextState.lastEvent,
Expand All @@ -111,7 +112,7 @@ export class VideoCapture {
}

/**
* Track a "Video Content Stopped" event every time the video stops playing
* Track a "[Amplitude] Content Stopped" event every time the video stops playing
* @returns The VideoCapture instance.
*/
captureVideoStopped(): VideoCapture {
Expand Down Expand Up @@ -212,9 +213,14 @@ export class VideoCapture {
duration: nextState.lastEvent?.duration ?? 0,
start_time: nextState.lastEvent?.start_time ?? 0,
position: nextState.position ?? 0,
delivery_mode: this.getDeliveryMode(),
};
}

private getDeliveryMode(): 'video' | 'audio' {
return this.videoEl instanceof HTMLAudioElement ? 'audio' : 'video';
}

parseStopEventProperties(nextState: VideoState): Record<string, string | number | boolean> {
const percentCompleted = ((nextState.position ?? 0) / (nextState.lastEvent?.duration ?? 0)) * 100;
return {
Expand All @@ -235,23 +241,23 @@ type UntrackVideoResult = () => void;
export type TrackVideoResult = UntrackVideoResult | Error;

/**
* Track video analytics events for an HTML video element or embedded video player.js instance.
* Track video analytics events for an HTML video or audio element or embedded video player.js instance.
*
* Captures Video Started and Video Stopped events.
* Captures [Amplitude] Content Started and [Amplitude] Content Stopped events.
*
* @experimental This function is experimental and may not be stable.
* @param amplitude - The Amplitude client instance.
* @param videoEl - The HTML video element or embedded video player.js instance to capture events from.
* @param videoEl - The HTML video or audio element or embedded video player.js instance to capture events from.
* @param options - The options for the video capture.
* @returns A function to stop the video capture.
*/
export function trackVideo(
amplitude: BrowserClient,
videoEl: HTMLVideoElement | EmbeddedVideoPlayer,
videoEl: HTMLMediaElement | EmbeddedVideoPlayer,
options: VideoCaptureOptions = {},
): TrackVideoResult {
const videoCapture = new VideoCapture(amplitude);
if (videoEl instanceof HTMLVideoElement) {
if (videoEl instanceof HTMLMediaElement) {
videoCapture.withVideoElement(videoEl);
} else {
videoCapture.withEmbeddedPlayer(videoEl);
Expand Down
50 changes: 48 additions & 2 deletions packages/analytics-browser/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ describe('config', () => {
},
topLevelDomain: '.amplitude.com',
enableRequestBodyCompression: false,
delayedEventsServerUrl: undefined,
delayedEventsServerUrl: 'https://delayed-events.prod.us-west-2.amplitude.com/2/httpapi/delayed',
});
expect(getTopLevelDomain).toHaveBeenCalledTimes(1);
});
Expand All @@ -169,6 +169,32 @@ describe('config', () => {
expect(config.delayedEventsServerUrl).toBe(delayedEventsServerUrl);
});

test('should default delayedEventsServerUrl for EU', async () => {
jest.spyOn(Config, 'getTopLevelDomain').mockResolvedValueOnce('.amplitude.com');
const config = await Config.useBrowserConfig(apiKey, { serverZone: 'EU' }, new AmplitudeBrowser());
expect(config.delayedEventsServerUrl).toBe(
'https://delayed-events.prod.eu-central-1.amplitude.com/2/httpapi/delayed',
);
});

test('should derive delayedEventsServerUrl from custom serverUrl', async () => {
jest.spyOn(Config, 'getTopLevelDomain').mockResolvedValueOnce('.amplitude.com');
const serverUrl = 'https://proxy.example.com/2/httpapi';
const config = await Config.useBrowserConfig(apiKey, { serverUrl }, new AmplitudeBrowser());
expect(config.delayedEventsServerUrl).toBe(`${serverUrl}/delayed`);
});

test('should prefer custom serverUrl over delayedEventsServerUrl', async () => {
jest.spyOn(Config, 'getTopLevelDomain').mockResolvedValueOnce('.amplitude.com');
const serverUrl = 'https://proxy.example.com/2/httpapi';
const config = await Config.useBrowserConfig(
apiKey,
{ serverUrl, delayedEventsServerUrl: 'https://example.com/2/httpapi/delayed' },
new AmplitudeBrowser(),
);
expect(config.delayedEventsServerUrl).toBe(`${serverUrl}/delayed`);
});

test('should fall back to memoryStorage when storageProvider is not enabled', async () => {
const localStorageIsEnabledSpy = jest
.spyOn(LocalStorageModule.LocalStorage.prototype, 'isEnabled')
Expand Down Expand Up @@ -293,7 +319,7 @@ describe('config', () => {
},
topLevelDomain: 'amplitude.com',
enableRequestBodyCompression: false,
delayedEventsServerUrl: undefined,
delayedEventsServerUrl: 'https://delayed-events.prod.us-west-2.amplitude.com/2/httpapi/delayed',
});
});
});
Expand Down Expand Up @@ -380,6 +406,26 @@ describe('config', () => {
});
});

describe('getDelayedEventsServerUrl', () => {
test('should default to the US delayed events endpoint', () => {
expect(Config.getDelayedEventsServerUrl(undefined, undefined)).toBe(
'https://delayed-events.prod.us-west-2.amplitude.com/2/httpapi/delayed',
);
});

test('should default unknown server zones to the US delayed events endpoint', () => {
expect(Config.getDelayedEventsServerUrl(undefined, undefined, 'STAGING' as never)).toBe(
'https://delayed-events.prod.us-west-2.amplitude.com/2/httpapi/delayed',
);
});

test('should append /delayed onto a serverUrl that already includes /2/httpapi', () => {
expect(Config.getDelayedEventsServerUrl('https://proxy.example.com/2/httpapi', undefined)).toBe(
'https://proxy.example.com/2/httpapi/delayed',
);
});
});

describe('createCookieStorage', () => {
test('should return cookies', async () => {
const storage = Config.createCookieStorage(DEFAULT_IDENTITY_STORAGE);
Expand Down
Loading
Loading