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: track time spent on a page #1876

Merged
merged 27 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
db562b0
feat: track time spent on a page
MoumitaM Oct 8, 2024
9c4a494
chore: moved the comment to appt place
MoumitaM Oct 8, 2024
02ffa04
refactor: trackPageLifecycleEvents fn
MoumitaM Oct 9, 2024
5813d01
chore: review comment addressed
MoumitaM Oct 9, 2024
f864f6a
chore: review comment addressed
MoumitaM Oct 9, 2024
eafe04f
chore: review comment addressed
MoumitaM Oct 9, 2024
5615d68
refactor: trackPageLifecycleEvents fn will be called from load api
MoumitaM Oct 14, 2024
e7e41aa
Merge branch 'develop' into feat/SDK-2478-track-timespent-on-a-page
MoumitaM Oct 14, 2024
72555d5
chore: addressed review comment
MoumitaM Oct 14, 2024
a390642
feat: modified load options structure
MoumitaM Oct 14, 2024
40c683c
chore: trackPageLifecycleEvents fn has been refactored and unit tests…
MoumitaM Oct 16, 2024
cc62338
chore: size limit updated
MoumitaM Oct 16, 2024
ab72611
Merge branch 'develop' into feat/SDK-2478-track-timespent-on-a-page
MoumitaM Oct 16, 2024
e4d5492
chore: typo corrected
MoumitaM Oct 16, 2024
6c7e207
chore: unit tests
MoumitaM Oct 17, 2024
8da4e56
Merge branch 'develop' into feat/SDK-2478-track-timespent-on-a-page
MoumitaM Oct 17, 2024
26a26c5
chore: size limit update
MoumitaM Oct 18, 2024
6455d95
chore: addressed review comments
MoumitaM Oct 18, 2024
56b1859
chore: eslint issue fixed
MoumitaM Oct 18, 2024
92b7442
fix: execution of cb when multiplt event listeners are attached
MoumitaM Oct 21, 2024
82fb915
chore: inline doc updated
MoumitaM Oct 21, 2024
6c9a94d
chore: readme updated
MoumitaM Oct 21, 2024
412beeb
Merge branch 'develop' into feat/SDK-2478-track-timespent-on-a-page
MoumitaM Oct 21, 2024
7c97388
chore: unit test added
MoumitaM Oct 22, 2024
90c97ec
Merge branch 'develop' into feat/SDK-2478-track-timespent-on-a-page
MoumitaM Oct 22, 2024
ebcc0c3
chore: unit test updated
MoumitaM Oct 22, 2024
7aea66e
Merge branch 'feat/SDK-2478-track-timespent-on-a-page' of https://git…
MoumitaM Oct 22, 2024
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: 10 additions & 0 deletions packages/analytics-js-common/src/types/ApplicationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ export type LifecycleState = {
dataPlaneUrl: Signal<string | undefined>;
};

export type AutoTrackState = {
saikumarrs marked this conversation as resolved.
Show resolved Hide resolved
pageLifecycle: PageLifecycleState;
};

export type PageLifecycleState = {
visitId: Signal<string | undefined>;
pageLoadedTimestamp: Signal<number | undefined>;
};

export type LoadOptionsState = Signal<LoadOptions>;

// TODO: define the metrics that we need to track
Expand Down Expand Up @@ -191,6 +200,7 @@ export interface ApplicationState {
storage: StorageState;
serverCookies: ServerCookiesState;
dataPlaneEvents: DataPlaneEventsState;
autoTrack: AutoTrackState;
}

export type DebouncedFunction = (...args: any[]) => void;
18 changes: 18 additions & 0 deletions packages/analytics-js-common/src/types/LoadOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,23 @@
events?: PreConsentEventsOptions;
};

export enum PageLifecycleEvents {
LOADED = 'Page Loaded',
UNLOADED = 'Page Unloaded',

Check warning on line 113 in packages/analytics-js-common/src/types/LoadOptions.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js-common/src/types/LoadOptions.ts#L112-L113

Added lines #L112 - L113 were not covered by tests
}

export type PageLifecycleOptions = {
enabled: boolean; // default false
events?: PageLifecycleEvents[];
options?: ApiOptions;
};

export type AutoTrackOptions = {
enabled?: boolean; // default false
options?: ApiOptions;
pageLifecycle?: PageLifecycleOptions;
};

/**
* Represents the options parameter in the load API
*/
Expand Down Expand Up @@ -150,6 +167,7 @@
externalAnonymousIdCookieName?: string;
useServerSideCookies?: boolean;
dataServiceEndpoint?: string;
autoTrack?: AutoTrackOptions;
};

export type ConsentOptions = {
Expand Down
8 changes: 4 additions & 4 deletions packages/analytics-js/.size-limit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default [
name: 'Core - Modern - NPM (CJS)',
path: 'dist/npm/modern/cjs/index.cjs',
import: '*',
limit: '24.5 KiB',
limit: '25 KiB',
},
{
name: 'Core - Modern - NPM (UMD)',
Expand All @@ -47,7 +47,7 @@ export default [
{
name: 'Core - Modern - CDN',
path: 'dist/cdn/modern/iife/rsa.min.js',
limit: '24.5 KiB',
limit: '25 KiB',
},
{
name: 'Core (Bundled) - Legacy - NPM (ESM)',
Expand Down Expand Up @@ -83,7 +83,7 @@ export default [
name: 'Core (Bundled) - Modern - NPM (UMD)',
path: 'dist/npm/modern/bundled/umd/index.js',
import: '*',
limit: '39 KiB',
limit: '39.5 KiB',
},
{
name: 'Core (Content Script) - Legacy - NPM (ESM)',
Expand Down Expand Up @@ -113,7 +113,7 @@ export default [
name: 'Core (Content Script) - Modern - NPM (CJS)',
path: 'dist/npm/modern/content-script/cjs/index.cjs',
import: '*',
limit: '38.5 KiB',
limit: '39 KiB',
},
{
name: 'Core (Content Script) - Modern - NPM (UMD)',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ describe('Logger', () => {
);
});

it(`should default the min log level to error if incorrectly set`, () => {
it(`should default the min log level to log if incorrectly set`, () => {
loggerInstance = new Logger();
loggerInstance.setMinLogLevel('dummy' as LogLevel);
loggerInstance.error('dummy error msg');
Expand All @@ -199,6 +199,6 @@ describe('Logger', () => {
);

loggerInstance.warn('dummy warn msg');
expect(console.warn).not.toHaveBeenCalled();
expect(console.warn).toHaveBeenCalled();
});
});
5 changes: 4 additions & 1 deletion packages/analytics-js/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,15 @@
// events: {
// delivery: 'buffer'
// }
}
},
// plugins: [
// 'StorageEncryption',
// 'StorageMigrator',
// 'XhrQueue'
// ]
// trackPageLifecycle:{
// enabled: true,
// }
};

rudderanalytics.load('__WRITE_KEY__', '__DATAPLANE_URL__', loadOptions);
Expand Down
109 changes: 101 additions & 8 deletions packages/analytics-js/src/app/RudderAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@
} from '@rudderstack/analytics-js-common/utilities/eventMethodOverloads';
import type { IRudderAnalytics } from '@rudderstack/analytics-js-common/types/IRudderAnalytics';
import type { Nullable } from '@rudderstack/analytics-js-common/types/Nullable';
import type {
AnonymousIdOptions,
ConsentOptions,
LoadOptions,
import {
PageLifecycleEvents,
type AnonymousIdOptions,
type ConsentOptions,
type LoadOptions,
} from '@rudderstack/analytics-js-common/types/LoadOptions';
import type { ApiCallback, ApiOptions } from '@rudderstack/analytics-js-common/types/EventApi';
import type { ApiObject } from '@rudderstack/analytics-js-common/types/ApiObject';
import { RS_APP } from '@rudderstack/analytics-js-common/constants/loggerContexts';
import { isString } from '@rudderstack/analytics-js-common/utilities/checks';
import type { IdentifyTraits } from '@rudderstack/analytics-js-common/types/traits';
import { generateUUID } from '@rudderstack/analytics-js-common/utilities/uuId';
import { onPageLeave } from '@rudderstack/analytics-js-common/utilities/page';
import { GLOBAL_PRELOAD_BUFFER } from '../constants/app';
import {
getPreloadedLoadEvent,
Expand All @@ -29,8 +32,13 @@
import type { IAnalytics } from '../components/core/IAnalytics';
import { Analytics } from '../components/core/Analytics';
import { defaultLogger } from '../services/Logger/Logger';
import { EMPTY_GROUP_CALL_ERROR, WRITE_KEY_NOT_A_STRING_ERROR } from '../constants/logMessages';
import {
EMPTY_GROUP_CALL_ERROR,
PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING,
WRITE_KEY_NOT_A_STRING_ERROR,
} from '../constants/logMessages';
import { defaultErrorHandler } from '../services/ErrorHandler';
import { state } from '../state';

// TODO: add analytics restart/reset mechanism

Expand Down Expand Up @@ -81,6 +89,9 @@

RudderAnalytics.globalSingleton = this;

state.autoTrack.pageLifecycle.visitId.value = generateUUID();
state.autoTrack.pageLifecycle.pageLoadedTimestamp.value = Date.now();

// start loading if a load event was buffered or wait for explicit load call
this.triggerBufferedLoadEvent();

Expand Down Expand Up @@ -129,18 +140,100 @@
}

this.setDefaultInstanceKey(writeKey);
// Track page loaded lifecycle event if enabled
this.trackPageLifecycleEvents(loadOptions);

this.analyticsInstances[writeKey] = new Analytics();
this.getAnalyticsInstance(writeKey).load(writeKey, dataPlaneUrl, loadOptions);
}

/**
* A function to get preloaded events array from global object
* @returns preloaded events array
*/
// eslint-disable-next-line class-methods-use-this
getPreloadedEvents() {
return Array.isArray((globalThis as typeof window).rudderanalytics)
? ((globalThis as typeof window).rudderanalytics as unknown as PreloadedEventCall[])
: ([] as PreloadedEventCall[]);
}

/**
* A function to track page lifecycle events like page loaded and page unloaded
* @param preloadedEventsArray
* @param loadOptions
* @returns
*/
trackPageLifecycleEvents(loadOptions?: Partial<LoadOptions>) {
const { autoTrack, useBeacon } = loadOptions ?? {};
const {
enabled: autoTrackEnabled = false,
options: autoTrackOptions = {},
pageLifecycle,
} = autoTrack ?? {};
const {
events = [PageLifecycleEvents.LOADED, PageLifecycleEvents.UNLOADED],
enabled = autoTrackEnabled,
options = autoTrackOptions,
} = pageLifecycle ?? {};

const visitId = state.autoTrack.pageLifecycle.visitId.value;
const pageLoadedTimestamp = state.autoTrack.pageLifecycle.pageLoadedTimestamp.value as number;

if (!enabled) {
return;
}

const preloadedEventsArray = this.getPreloadedEvents();

Check warning on line 187 in packages/analytics-js/src/app/RudderAnalytics.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/app/RudderAnalytics.ts#L187

Added line #L187 was not covered by tests
MoumitaM marked this conversation as resolved.
Show resolved Hide resolved

// track page loaded event
if (events.length === 0 || events.includes(PageLifecycleEvents.LOADED)) {
preloadedEventsArray.unshift([

Check warning on line 191 in packages/analytics-js/src/app/RudderAnalytics.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/app/RudderAnalytics.ts#L191

Added line #L191 was not covered by tests
'track',
PageLifecycleEvents.LOADED,
{ visitId },
{
...options,
originalTimestamp: new Date(pageLoadedTimestamp).toISOString(),
},
]);
}

// track page unloaded event
if (events.length === 0 || events.includes(PageLifecycleEvents.UNLOADED)) {
if (useBeacon === true) {
// Register the page unloaded lifecycle event listeners
onPageLeave((isAccessible: boolean) => {

Check warning on line 206 in packages/analytics-js/src/app/RudderAnalytics.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/app/RudderAnalytics.ts#L206

Added line #L206 was not covered by tests
if (isAccessible === false && state.lifecycle.loaded.value) {
const pageUnloadedTimestamp = Date.now();
const visitDuration = pageUnloadedTimestamp - pageLoadedTimestamp;
this.track(

Check warning on line 210 in packages/analytics-js/src/app/RudderAnalytics.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/app/RudderAnalytics.ts#L208-L210

Added lines #L208 - L210 were not covered by tests
PageLifecycleEvents.UNLOADED,
{
visitId,
visitDuration,
},
{
...options,
originalTimestamp: new Date(pageUnloadedTimestamp).toISOString(),
},
);
}
});
} else {
// throw warning if beacon is disabled
this.logger.warn(PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING(RS_APP));

Check warning on line 225 in packages/analytics-js/src/app/RudderAnalytics.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/app/RudderAnalytics.ts#L225

Added line #L225 was not covered by tests
}
}
setExposedGlobal(GLOBAL_PRELOAD_BUFFER, clone(preloadedEventsArray));

Check warning on line 228 in packages/analytics-js/src/app/RudderAnalytics.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/app/RudderAnalytics.ts#L228

Added line #L228 was not covered by tests
}
MoumitaM marked this conversation as resolved.
Show resolved Hide resolved

/**
* Trigger load event in buffer queue if exists and stores the
* remaining preloaded events array in global object
*/
triggerBufferedLoadEvent() {
const preloadedEventsArray = Array.isArray((globalThis as typeof window).rudderanalytics)
? ((globalThis as typeof window).rudderanalytics as unknown as PreloadedEventCall[])
: ([] as PreloadedEventCall[]);
const preloadedEventsArray = this.getPreloadedEvents();

// The array will be mutated in the below method
promotePreloadedConsentEventsToTop(preloadedEventsArray);
Expand Down
6 changes: 2 additions & 4 deletions packages/analytics-js/src/components/core/Analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
trackArgumentsToCallOptions,
} from '@rudderstack/analytics-js-common/utilities/eventMethodOverloads';
import { BufferQueue } from '@rudderstack/analytics-js-common/services/BufferQueue/BufferQueue';
import { defaultLogger } from '../../services/Logger';
import { POST_LOAD_LOG_LEVEL, defaultLogger } from '../../services/Logger';
import { defaultErrorHandler } from '../../services/ErrorHandler';
import { defaultPluginEngine } from '../../services/PluginEngine';
import { PluginsManager } from '../pluginsManager';
Expand Down Expand Up @@ -127,9 +127,7 @@ class Analytics implements IAnalytics {
});

// set log level as early as possible
if (state.loadOptions.value.logLevel) {
this.logger?.setMinLogLevel(state.loadOptions.value.logLevel);
}
this.logger?.setMinLogLevel(state.loadOptions.value.logLevel ?? POST_LOAD_LOG_LEVEL);

// Expose state to global objects
setExposedGlobal('state', state, writeKey);
Expand Down
4 changes: 4 additions & 0 deletions packages/analytics-js/src/constants/logMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@
const BAD_COOKIES_WARNING = (key: string) =>
`The cookie data for ${key} seems to be encrypted using SDK versions < v3. The data is dropped. This can potentially stem from using SDK versions < v3 on other sites or web pages that can share cookies with this webpage. We recommend using the same SDK (v3) version everywhere or avoid disabling the storage data migration.`;

const PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING = (context: string) =>
`${context}${LOG_CONTEXT_SEPARATOR}Page Unloaded event can only be tracked when the Beacon transport is active. Please enable "useBeacon" load API option.`;

Check warning on line 261 in packages/analytics-js/src/constants/logMessages.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/constants/logMessages.ts#L261

Added line #L261 was not covered by tests

export {
UNSUPPORTED_CONSENT_MANAGER_ERROR,
UNSUPPORTED_ERROR_REPORTING_PROVIDER_WARNING,
Expand Down Expand Up @@ -321,4 +324,5 @@
COMPONENT_BASE_URL_ERROR,
SERVER_SIDE_COOKIE_FEATURE_OVERRIDE_WARNING,
BAD_COOKIES_WARNING,
PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING,
};
4 changes: 3 additions & 1 deletion packages/analytics-js/src/services/Logger/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ const LOG_LEVEL_MAP: Record<LogLevel, number> = {
NONE: 5,
};

const DEFAULT_LOG_LEVEL = 'ERROR';
const DEFAULT_LOG_LEVEL = 'LOG';
MoumitaM marked this conversation as resolved.
Show resolved Hide resolved
const POST_LOAD_LOG_LEVEL = 'ERROR';
MoumitaM marked this conversation as resolved.
Show resolved Hide resolved
const LOG_MSG_PREFIX = 'RS SDK';
const LOG_MSG_PREFIX_STYLE = 'font-weight: bold; background: black; color: white;';
const LOG_MSG_STYLE = 'font-weight: normal;';
Expand Down Expand Up @@ -120,4 +121,5 @@ export {
LOG_MSG_PREFIX_STYLE,
LOG_MSG_STYLE,
defaultLogger,
POST_LOAD_LOG_LEVEL,
};
8 changes: 7 additions & 1 deletion packages/analytics-js/src/services/Logger/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export { Logger, DEFAULT_LOG_LEVEL, LOG_LEVEL_MAP, defaultLogger } from './Logger';
export {
Logger,
DEFAULT_LOG_LEVEL,
LOG_LEVEL_MAP,
defaultLogger,
POST_LOAD_LOG_LEVEL,
} from './Logger';
3 changes: 3 additions & 0 deletions packages/analytics-js/src/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { pluginsState } from './slices/plugins';
import { storageState } from './slices/storage';
import { serverSideCookiesState } from './slices/serverCookies';
import { dataPlaneEventsState } from './slices/dataPlaneEvents';
import { autoTrackState } from './slices/autoTrack';

const defaultStateValues: ApplicationState = {
capabilities: capabilitiesState,
Expand All @@ -32,6 +33,7 @@ const defaultStateValues: ApplicationState = {
storage: storageState,
serverCookies: serverSideCookiesState,
dataPlaneEvents: dataPlaneEventsState,
autoTrack: autoTrackState,
};

const state: ApplicationState = {
Expand All @@ -54,6 +56,7 @@ const resetState = () => {
state.storage = clone(defaultStateValues.storage);
state.serverCookies = clone(defaultStateValues.serverCookies);
state.dataPlaneEvents = clone(defaultStateValues.dataPlaneEvents);
state.autoTrack = clone(defaultStateValues.autoTrack);
};

export { state, resetState };
11 changes: 11 additions & 0 deletions packages/analytics-js/src/state/slices/autoTrack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { signal } from '@preact/signals-core';
import type { AutoTrackState } from '@rudderstack/analytics-js-common/types/ApplicationState';

const autoTrackState: AutoTrackState = {
pageLifecycle: {
visitId: signal(undefined),
pageLoadedTimestamp: signal(undefined),
},
};

export { autoTrackState };
Loading