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
2 changes: 2 additions & 0 deletions packages/experiment-tag/src/message-bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ export type ElementVisiblePayload = { mutationList: MutationRecord[] };
export type AnalyticsEventPayload = AnalyticsEvent;
export type ManualTriggerPayload = { name: string };
export type UrlChangePayload = { updateActivePages?: boolean };
export type TimeOnPagePayload = { durationMs: number };

export type MessagePayloads = {
element_appeared: ElementAppearedPayload;
element_visible: ElementVisiblePayload;
url_change: UrlChangePayload;
analytics_event: AnalyticsEventPayload;
manual: ManualTriggerPayload;
time_on_page: TimeOnPagePayload;
};

export type MessageType = keyof MessagePayloads;
Expand Down
35 changes: 34 additions & 1 deletion packages/experiment-tag/src/subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ElementAppearedPayload,
ManualTriggerPayload,
MessageType,
TimeOnPagePayload,
} from './message-bus';
import { DebouncedMutationManager } from './mutation-manager';
import {
Expand All @@ -15,6 +16,7 @@ import {
ManualTriggerValue,
PageObject,
PageObjects,
TimeOnPageTriggerValue,
} from './types';

const evaluationEngine = new EvaluationEngine();
Expand All @@ -41,6 +43,7 @@ export class SubscriptionManager {
private elementVisibilityState: Map<string, boolean> = new Map();
private elementAppearedState: Map<string, boolean> = new Map();
private activeElementSelectors: Set<string> = new Set();
private timeOnPageTimeouts: Set<ReturnType<typeof setTimeout>> = new Set();

constructor(
webExperimentClient: DefaultWebExperimentClient,
Expand All @@ -67,9 +70,10 @@ export class SubscriptionManager {
this.setupMutationObserverPublisher();
this.setupVisibilityPublisher();
this.setupPageObjectSubscriptions();
this.setupUrlChangeReset();
// Initial check for elements that already exist
this.checkInitialElements();
this.setUpTimeOnPagePublisher();
this.setupUrlChangeReset();
};

/**
Expand Down Expand Up @@ -188,6 +192,7 @@ export class SubscriptionManager {
);
this.setupVisibilityPublisher();
this.checkInitialElements();
this.setUpTimeOnPagePublisher();
});
};

Expand Down Expand Up @@ -413,6 +418,28 @@ export class SubscriptionManager {
wrapHistoryMethods();
};

setUpTimeOnPagePublisher = () => {
// Clear any existing timeouts first
this.timeOnPageTimeouts.forEach((timeoutId) => clearTimeout(timeoutId));
this.timeOnPageTimeouts.clear();
// Publish initial time_on_page event
this.messageBus.publish('time_on_page');

for (const pages of Object.values(this.pageObjects)) {
for (const page of Object.values(pages)) {
if (page.trigger_type === 'time_on_page') {
const triggerValue = page.trigger_value as TimeOnPageTriggerValue;
const durationMs = triggerValue.durationMs;
const timeoutId = setTimeout(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. It's missing visibilitychange event handler to reset when the user moves away from the current tab
  2. Q: When there are multiple page objects with the time on page trigger type, are there any potential issues?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. should time on page be cumulative? Or should the trigger be reset to 0 if it has not already fired?
  2. there should not be issues with this, but deduping of page object-triggered actions will be more formally addressed in another PR.

Copy link
Collaborator

@stephen-choi-amplitude stephen-choi-amplitude Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Per tech doc:

The timer resets whenever the user leaves the tab/window and returns

IOW, clear timeout when the current tab is hidden, and restart

this.messageBus.publish('time_on_page', { durationMs });
this.timeOnPageTimeouts.delete(timeoutId);
}, durationMs);
this.timeOnPageTimeouts.add(timeoutId);
}
}
}
};

private isPageObjectActive = <T extends MessageType>(
page: PageObject,
message: MessagePayloads[T],
Expand Down Expand Up @@ -501,6 +528,12 @@ export class SubscriptionManager {
return this.elementVisibilityState.get(observerKey) ?? false;
}

case 'time_on_page': {
const triggerValue = page.trigger_value as TimeOnPageTriggerValue;
const triggerPayload = message as TimeOnPagePayload;
return triggerPayload.durationMs >= triggerValue.durationMs;
}

default:
return false;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/experiment-tag/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export interface ManualTriggerValue {
name: string;
}

export interface TimeOnPageTriggerValue {
durationMs: number;
}

export type PageObject = {
id: string;
name: string;
Expand All @@ -54,6 +58,7 @@ export type PageObject = {
| ElementAppearedTriggerValue
| ElementVisibleTriggerValue
| ManualTriggerValue
| TimeOnPageTriggerValue
| Record<string, unknown>;
};

Expand Down
Loading