Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add time on site #975

Merged
merged 15 commits into from
Feb 13, 2025
Merged
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
21,266 changes: 1,661 additions & 19,605 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"@babel/preset-env": "^7.6.0",
"@babel/preset-typescript": "^7.6.0",
"@mparticle/data-planning-models": "^0.1.0",
"@mparticle/event-models": "^1.1.8",
"@mparticle/event-models": "^1.1.9",
"@rollup/plugin-babel": "6.0.3",
"@rollup/plugin-commonjs": "25.0.4",
"@rollup/plugin-json": "^5.0.2",
Expand Down
113 changes: 113 additions & 0 deletions src/foregroundTimeTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { isNumber } from './utils';
import { LocalStorageVault } from './vault';

export default class ForegroundTimeTracker {
rmi22186 marked this conversation as resolved.
Show resolved Hide resolved
private isTrackerActive: boolean = false;
private localStorageName: string = '';
private timerVault: LocalStorageVault<number>;
public startTime: number = 0;
public totalTime: number = 0;

constructor(timerKey: string) {
this.localStorageName = `mp-time-${timerKey}`;
this.timerVault = new LocalStorageVault<number>(this.localStorageName);
this.loadTimeFromStorage();
this.addHandlers();
if (document.hidden === false) {
this.startTracking();
}
}

private addHandlers(): void {
// when user switches tabs or minimizes the window
document.addEventListener('visibilitychange', () =>
this.handleVisibilityChange()
);
// when user switches to another application
window.addEventListener('blur', () => this.handleWindowBlur());
// when window gains focus
window.addEventListener('focus', () => this.handleWindowFocus());
// this ensures that timers between tabs are in sync
window.addEventListener('storage', event => this.syncAcrossTabs(event));
// when user closes tab, refreshes, or navigates to another page via link
window.addEventListener('beforeunload', () =>
this.updateTimeInPersistence()
);
}

private handleVisibilityChange(): void {
if (document.hidden) {
this.stopTracking();
} else {
this.startTracking();
}
}

private handleWindowBlur(): void {
if (this.isTrackerActive) {
this.stopTracking();
}
}

private handleWindowFocus(): void {
if (!this.isTrackerActive) {
this.startTracking();
}
}

private syncAcrossTabs(event: StorageEvent): void {
if (event.key === this.localStorageName && event.newValue !== null) {
const newTime = parseFloat(event.newValue) || 0;
this.totalTime = newTime;
}
}

public updateTimeInPersistence(): void {
if (this.isTrackerActive) {
this.timerVault.store(Math.round(this.totalTime));
}
}

private loadTimeFromStorage(): void {
const storedTime = this.timerVault.retrieve();
if (isNumber(storedTime) && storedTime !== null) {
this.totalTime = storedTime;
}
}


private startTracking(): void {
if (!document.hidden) {
this.startTime = Math.floor(performance.now());
this.isTrackerActive = true;
}
}

private stopTracking(): void {
if (this.isTrackerActive) {
this.setTotalTime();
this.updateTimeInPersistence();
this.isTrackerActive = false;
}
}

private setTotalTime(): void {
if (this.isTrackerActive) {
const now = Math.floor(performance.now());
this.totalTime += now - this.startTime;
this.startTime = now;

}
}

public getTimeInForeground(): number {
this.setTotalTime();
this.updateTimeInPersistence();
return this.totalTime;
}

public resetTimer(): void {
this.totalTime = 0;
this.updateTimeInPersistence();
}
}
3 changes: 3 additions & 0 deletions src/mockBatchCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export default class _BatchValidator {
},
_resetForTests: mockFunction,
_APIClient: null,
_timeOnSiteTimer: {
getTimeInForeground: mockFunction
},
MPSideloadedKit: null,
_Consent: null,
_Events: null,
Expand Down
2 changes: 2 additions & 0 deletions src/mp-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { IEvents } from './events.interfaces';
import { IECommerce } from './ecommerce.interfaces';
import { INativeSdkHelpers } from './nativeSdkHelpers.interfaces';
import { IPersistence } from './persistence.interfaces';
import ForegroundTimer from './foregroundTimeTracker';

export interface IErrorLogMessage {
message?: string;
Expand Down Expand Up @@ -84,6 +85,7 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK {
_Store: IStore;
_instanceName: string;
_preInit: IPreInit;
_timeOnSiteTimer: ForegroundTimer;
}

const { Messages, HTTPCodes, FeatureFlags } = Constants;
Expand Down
2 changes: 2 additions & 0 deletions src/sdkRuntimeModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ export interface SDKEvent {
DataPlan?: SDKDataPlan;
LaunchReferral?: string;
ExpandedEventCount: number;
ActiveTimeOnSite: number;
}

export interface SDKGeoLocation {
lat: number | string;
lng: number | string;
Expand Down
1 change: 1 addition & 0 deletions src/sdkToEventsApiConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,7 @@ export function convertBaseEventData(
custom_attributes: sdkEvent.EventAttributes,
location: convertSDKLocation(sdkEvent.Location),
source_message_id: sdkEvent.SourceMessageId,
active_time_on_site_ms: sdkEvent.ActiveTimeOnSite
};

return commonEventData;
Expand Down
1 change: 1 addition & 0 deletions src/serverModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ export default function ServerModel(
event.data,
event.name
),
ActiveTimeOnSite: mpInstance._timeOnSiteTimer?.getTimeInForeground(),
SourceMessageId:
event.sourceMessageId ||
mpInstance._Helpers.generateUniqueId(),
Expand Down
9 changes: 8 additions & 1 deletion src/sessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,20 @@ export default function SessionManager(
});

mpInstance._Store.nullifySession();
mpInstance._timeOnSiteTimer?.resetTimer();
return;
}

if (!mpInstance._Helpers.canLog()) {
// At this moment, an AbandonedEndSession is defined when on of three things occurs:
// At this moment, an AbandonedEndSession is defined when one of three things occurs:
// - the SDK's store is not enabled because mParticle.setOptOut was called
// - the devToken is undefined
// - webviewBridgeEnabled is set to false
mpInstance.Logger.verbose(
Messages.InformationMessages.AbandonEndSession
);
mpInstance._timeOnSiteTimer?.resetTimer();

return;
}

Expand All @@ -155,6 +158,8 @@ export default function SessionManager(
mpInstance.Logger.verbose(
Messages.InformationMessages.NoSessionToEnd
);
mpInstance._timeOnSiteTimer?.resetTimer();

return;
}

Expand All @@ -180,6 +185,8 @@ export default function SessionManager(
mpInstance._Store.nullifySession();
}
}

mpInstance._timeOnSiteTimer?.resetTimer();
};

this.setSessionTimer = function(): void {
Expand Down
2 changes: 2 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
} from './persistence.interfaces';
import { CookieSyncDates, IPixelConfiguration } from './cookieSyncManager';
import { IMParticleWebSDKInstance } from './mp-instance';
import ForegroundTimer from './foregroundTimeTracker';

// This represents the runtime configuration of the SDK AFTER
// initialization has been complete and all settings and
Expand Down Expand Up @@ -680,6 +681,7 @@ export default function Store(

if (workspaceToken) {
this.SDKConfig.workspaceToken = workspaceToken;
mpInstance._timeOnSiteTimer = new ForegroundTimer(workspaceToken);
} else {
mpInstance.Logger.warning(
'You should have a workspaceToken on your config object for security purposes.'
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const event0: SDKEvent = {
Debug: false,
DeviceId: 'test-device',
Timestamp: 0,
ActiveTimeOnSite: 10
};

export const event1: SDKEvent = {
Expand All @@ -42,6 +43,7 @@ export const event1: SDKEvent = {
Debug: false,
DeviceId: 'test-device',
Timestamp: 0,
ActiveTimeOnSite: 20
};

export const event2: SDKEvent = {
Expand All @@ -64,6 +66,7 @@ export const event2: SDKEvent = {
Debug: false,
DeviceId: 'test-device',
Timestamp: 0,
ActiveTimeOnSite: 30
};

export const event3: SDKEvent = {
Expand All @@ -86,4 +89,5 @@ export const event3: SDKEvent = {
Debug: false,
DeviceId: 'test-device',
Timestamp: 0,
ActiveTimeOnSite: 40
};
Loading
Loading