Skip to content

Commit

Permalink
feat: track time spent on a page
Browse files Browse the repository at this point in the history
  • Loading branch information
MoumitaM committed Oct 8, 2024
1 parent cc239cf commit db562b0
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 11 deletions.
12 changes: 12 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,17 @@ export type PreConsentOptions = {
events?: PreConsentEventsOptions;
};

export enum PageLifecycleEvents {
PAGELOADED = 'Page Loaded',
PAGEUNLOADED = '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 TrackPageLifecycleOptions = {
enabled: boolean; // default false
events?: PageLifecycleEvents[];
options?: ApiOptions;
};

/**
* Represents the options parameter in the load API
*/
Expand Down Expand Up @@ -150,6 +161,7 @@ export type LoadOptions = {
externalAnonymousIdCookieName?: string;
useServerSideCookies?: boolean;
dataServiceEndpoint?: string;
trackPageLifecycle?: TrackPageLifecycleOptions;
};

export type ConsentOptions = {
Expand Down
6 changes: 3 additions & 3 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 @@ -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
5 changes: 4 additions & 1 deletion packages/analytics-js/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,15 @@
// events: {
// delivery: 'buffer'
// }
}
},
// plugins: [
// 'StorageEncryption',
// 'StorageMigrator',
// 'XhrQueue'
// ]
// trackPageLifecycle:{
// enabled: true,
// }
};

rudderanalytics.load('__WRITE_KEY__', '__DATAPLANE_URL__', loadOptions);
Expand Down
90 changes: 84 additions & 6 deletions packages/analytics-js/src/app/RudderAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ import {
} 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 { setExposedGlobal } from '../components/utilities/globals';
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 @@ -80,7 +88,7 @@ class RudderAnalytics implements IRudderAnalytics<IAnalytics> {
this.consent = this.consent.bind(this);

RudderAnalytics.globalSingleton = this;

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

Expand Down Expand Up @@ -133,6 +141,74 @@ class RudderAnalytics implements IRudderAnalytics<IAnalytics> {
this.getAnalyticsInstance(writeKey).load(writeKey, dataPlaneUrl, loadOptions);
}

/**
* A function to track page lifecycle events like page loaded and page unloaded
* @param preloadedEventsArray
* @param loadOptions
* @returns
*/
// eslint-disable-next-line class-methods-use-this
trackPageLifecycleEvents(
preloadedEventsArray: PreloadedEventCall[],
loadOptions?: Partial<LoadOptions>,
) {
const { trackPageLifecycle, useBeacon } = loadOptions ?? {};
const { events = [], enabled = false, options = {} } = trackPageLifecycle ?? {};
if (enabled) {
const visitId = generateUUID();
const pageLoadedTimestamp = Date.now();

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

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/app/RudderAnalytics.ts#L158-L159

Added lines #L158 - L159 were not covered by tests
if (events.length === 0 || events.includes(PageLifecycleEvents.PAGELOADED)) {
preloadedEventsArray.unshift([

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L161 was not covered by tests
'track',
PageLifecycleEvents.PAGELOADED,
{ visitId },
{
originalTimestamp: new Date(pageLoadedTimestamp).toISOString(),
...options,
},
]);
}
if (events.length === 0 || events.includes(PageLifecycleEvents.PAGEUNLOADED)) {
// throw warning if beacon is disabled
if (useBeacon === true) {
// Register the page unloaded lifecycle event listeners
onPageLeave((isAccessible: boolean) => {

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L175 was not covered by tests
if (isAccessible === false) {
const visitDuration = Date.now() - pageLoadedTimestamp;

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L177 was not covered by tests
if (!state.lifecycle.loaded.value) {
preloadedEventsArray.unshift([

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L179 was not covered by tests
'track',
PageLifecycleEvents.PAGEUNLOADED,
{
visitId,
visitDuration,
},
{
...options,
},
]);
} else {
this.track(

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
PageLifecycleEvents.PAGEUNLOADED,
{
visitId,
visitDuration,
},
{
...options,
},
);
}
}
});
} else {
this.logger.warn(PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING());

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

View check run for this annotation

Codecov / codecov/patch

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

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

Check warning on line 208 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

Added line #L208 was not covered by tests
}
}

/**
* Trigger load event in buffer queue if exists and stores the
* remaining preloaded events array in global object
Expand All @@ -155,6 +231,8 @@ class RudderAnalytics implements IRudderAnalytics<IAnalytics> {

// Process load method if present in the buffered requests
if (loadEvent.length > 0) {
// Track page loaded lifecycle event if enabled
this.trackPageLifecycleEvents(preloadedEventsArray, loadEvent[3]);
// Remove the event name from the Buffered Event array and keep only arguments
loadEvent.shift();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down
8 changes: 8 additions & 0 deletions packages/analytics-js/src/components/utilities/loadOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ const normalizeLoadOptions = (
normalizedLoadOpts.preConsent = removeUndefinedAndNullValues(normalizedLoadOpts.preConsent);
}

if (!isObjectLiteralAndNotNull(normalizedLoadOpts.trackPageLifecycle)) {
delete normalizedLoadOpts.trackPageLifecycle;
} else {
normalizedLoadOpts.trackPageLifecycle = removeUndefinedAndNullValues(

Check warning on line 127 in packages/analytics-js/src/components/utilities/loadOptions.ts

View check run for this annotation

Codecov / codecov/patch

packages/analytics-js/src/components/utilities/loadOptions.ts#L127

Added line #L127 was not covered by tests
normalizedLoadOpts.trackPageLifecycle,
);
}

const mergedLoadOptions: LoadOptions = mergeDeepRight(loadOptionsFromState, normalizedLoadOpts);

return mergedLoadOptions;
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 INVALID_POLYFILL_URL_WARNING = (
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 = (): string =>
`Page Unloaded event is only supported for Beacon transport mechanism. Please set useBeacon to true to enable this feature.`;

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 @@ export {
COMPONENT_BASE_URL_ERROR,
SERVER_SIDE_COOKIE_FEATURE_OVERRIDE_WARNING,
BAD_COOKIES_WARNING,
PAGE_UNLOAD_ON_BEACON_DISABLED_WARNING,
};
9 changes: 8 additions & 1 deletion packages/analytics-js/src/state/slices/loadOptions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { signal } from '@preact/signals-core';
import { clone } from 'ramda';
import type { LoadOptions } from '@rudderstack/analytics-js-common/types/LoadOptions';
import {
PageLifecycleEvents,
type LoadOptions,
} from '@rudderstack/analytics-js-common/types/LoadOptions';
import type { LoadOptionsState } from '@rudderstack/analytics-js-common/types/ApplicationState';
import {
DEFAULT_DATA_PLANE_EVENTS_BUFFER_TIMEOUT_MS,
Expand Down Expand Up @@ -39,6 +42,10 @@ const defaultLoadOptions: LoadOptions = {
},
sendAdblockPageOptions: {},
useServerSideCookies: false,
trackPageLifecycle: {
enabled: false,
events: [PageLifecycleEvents.PAGELOADED, PageLifecycleEvents.PAGEUNLOADED],
},
};

const loadOptionsState: LoadOptionsState = signal(clone(defaultLoadOptions));
Expand Down

0 comments on commit db562b0

Please sign in to comment.