Skip to content
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
11 changes: 11 additions & 0 deletions api-docs/docs/browser-tracker/browser-tracker.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@ export type ActivityCallbackData = {
minYOffset: number;
maxXOffset: number;
maxYOffset: number;
activityMetrics?: ActivityMetrics;
};

// @public
export type ActivityMetrics = {
mouseDistance: number;
scrollDistance: number;
keyPresses: number;
clicks: number;
touches: number;
};

// @public
export interface ActivityTrackingConfiguration {
activityMetrics?: boolean;
heartbeatDelay: number;
minimumVisitLength: number;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"comment": "Add opt-in activity metrics tracking with activity_metrics entity",
"type": "none",
"packageName": "@snowplow/browser-tracker-core"
}
],
"packageName": "@snowplow/browser-tracker-core"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"comment": "Add opt-in activity metrics tracking with activity_metrics entity",
"type": "none",
"packageName": "@snowplow/browser-tracker"
}
],
"packageName": "@snowplow/browser-tracker"
}
130 changes: 111 additions & 19 deletions libraries/browser-tracker-core/src/tracker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
PageViewEvent,
ActivityCallback,
ActivityCallbackData,
ActivityMetrics,
TrackerConfiguration,
BrowserTracker,
ActivityTrackingConfiguration,
Expand Down Expand Up @@ -65,7 +66,13 @@ import {
emptyIdCookie,
eventIndexFromIdCookie,
} from './id_cookie';
import { CLIENT_SESSION_SCHEMA, WEB_PAGE_SCHEMA, BROWSER_CONTEXT_SCHEMA, APPLICATION_CONTEXT_SCHEMA } from './schemata';
import {
CLIENT_SESSION_SCHEMA,
WEB_PAGE_SCHEMA,
BROWSER_CONTEXT_SCHEMA,
APPLICATION_CONTEXT_SCHEMA,
ACTIVITY_METRICS_SCHEMA,
} from './schemata';
import { getBrowserProperties } from '../helpers/browser_props';
import { asyncCookieStorage, syncCookieStorage } from './cookie_storage';

Expand All @@ -88,6 +95,8 @@ type ActivityConfig = {
configHeartBeatTimer: number;
/** The setInterval identifier */
activityInterval?: number;
/** Whether activity metrics tracking is enabled for this configuration */
activityMetrics?: boolean;
};

/** The configurations for the two types of Activity Tracking */
Expand Down Expand Up @@ -264,6 +273,14 @@ export function Tracker(
maxXOffset: number,
minYOffset: number,
maxYOffset: number,
// Activity metrics tracking state
activityMetricsState = {
metrics: { mouseDistance: 0, scrollDistance: 0, keyPresses: 0, clicks: 0, touches: 0 } as ActivityMetrics,
lastMouseX: undefined as number | undefined,
lastMouseY: undefined as number | undefined,
lastScrollX: undefined as number | undefined,
lastScrollY: undefined as number | undefined,
},
// Domain hash value
domainHash: string,
// Domain unique user ID
Expand Down Expand Up @@ -514,17 +531,66 @@ export function Tracker(
* Process all "activity" events.
* For performance, this function must have low overhead.
*/
function activityHandler() {
function activityHandler(event?: Event) {
const now = new Date();
lastActivityTime = now.getTime();

if (isActivityMetricsEnabled() && event) {
switch (event.type) {
case 'mousemove': {
const me = event as MouseEvent;
if (activityMetricsState.lastMouseX !== undefined && activityMetricsState.lastMouseY !== undefined) {
const dx = me.clientX - activityMetricsState.lastMouseX;
const dy = me.clientY - activityMetricsState.lastMouseY;
activityMetricsState.metrics.mouseDistance += Math.sqrt(dx * dx + dy * dy);
}
activityMetricsState.lastMouseX = me.clientX;
activityMetricsState.lastMouseY = me.clientY;
break;
}
case 'click':
activityMetricsState.metrics.clicks++;
break;
case 'keydown':
activityMetricsState.metrics.keyPresses++;
break;
case 'touchstart':
activityMetricsState.metrics.touches++;
break;
// scroll handled in scrollHandler
}
}
}

/*
* Process all "scroll" events.
*/
function scrollHandler() {
updateMaxScrolls();
activityHandler();

const offsets = getPageOffsets();

const x = offsets[0];
if (x < minXOffset) {
minXOffset = x;
} else if (x > maxXOffset) {
maxXOffset = x;
}

const y = offsets[1];
if (y < minYOffset) {
minYOffset = y;
} else if (y > maxYOffset) {
maxYOffset = y;
}

if (isActivityMetricsEnabled()) {
if (activityMetricsState.lastScrollX !== undefined && activityMetricsState.lastScrollY !== undefined) {
activityMetricsState.metrics.scrollDistance += Math.abs(x - activityMetricsState.lastScrollX) + Math.abs(y - activityMetricsState.lastScrollY);
}
activityMetricsState.lastScrollX = x;
activityMetricsState.lastScrollY = y;
}
}

/*
Expand Down Expand Up @@ -555,24 +621,35 @@ export function Tracker(
}

/*
* Check the max scroll levels, updating as necessary
* Whether activity metrics tracking is enabled for any active configuration
*/
function updateMaxScrolls() {
const offsets = getPageOffsets();
function isActivityMetricsEnabled(): boolean {
return !!(
activityTrackingConfig.configurations.pagePing?.activityMetrics ||
activityTrackingConfig.configurations.callback?.activityMetrics
);
}

const x = offsets[0];
if (x < minXOffset) {
minXOffset = x;
} else if (x > maxXOffset) {
maxXOffset = x;
}
/*
* Reset activity metrics accumulators and position trackers
*/
function resetActivityMetricsState() {
activityMetricsState.metrics = { mouseDistance: 0, scrollDistance: 0, keyPresses: 0, clicks: 0, touches: 0 };
activityMetricsState.lastMouseX = undefined;
activityMetricsState.lastMouseY = undefined;
activityMetricsState.lastScrollX = undefined;
activityMetricsState.lastScrollY = undefined;
}

const y = offsets[1];
if (y < minYOffset) {
minYOffset = y;
} else if (y > maxYOffset) {
maxYOffset = y;
}
/*
* Return a snapshot of current activity metrics
*/
function getActivityMetrics(): ActivityMetrics {
return {
...activityMetricsState.metrics,
mouseDistance: Math.round(activityMetricsState.metrics.mouseDistance),
scrollDistance: Math.round(activityMetricsState.metrics.scrollDistance),
};
}

/*
Expand Down Expand Up @@ -1098,6 +1175,7 @@ export function Tracker(

// Capture our initial scroll points
resetMaxScrolls();
resetActivityMetricsState();

// Add event handlers; cross-browser compatibility here varies significantly
// @see http://quirksmode.org/dom/events
Expand Down Expand Up @@ -1126,6 +1204,7 @@ export function Tracker(
if (activityTrackingConfig.enabled && (resetActivityTrackingOnPageView || installingActivityTracking)) {
// Periodic check for activity.
lastActivityTime = now.getTime();
resetActivityMetricsState();

let key: keyof ActivityConfigurations;
for (key in activityTrackingConfig.configurations) {
Expand All @@ -1147,8 +1226,16 @@ export function Tracker(
) {
const executePagePing = (cb: ActivityCallback, context: Array<SelfDescribingJson>) => {
refreshUrl();
cb({ context, pageViewId: getPageViewId(), minXOffset, minYOffset, maxXOffset, maxYOffset });
let activityMetrics: ActivityMetrics | undefined;
if (isActivityMetricsEnabled()) {
activityMetrics = getActivityMetrics();
context = context.concat([{ schema: ACTIVITY_METRICS_SCHEMA, data: activityMetrics }]);
}
cb({ context, pageViewId: getPageViewId(), minXOffset, minYOffset, maxXOffset, maxYOffset, activityMetrics });
resetMaxScrolls();
if (isActivityMetricsEnabled()) {
resetActivityMetricsState();
}
Comment thread
stanch marked this conversation as resolved.
};

const timeout = () => {
Expand Down Expand Up @@ -1192,6 +1279,7 @@ export function Tracker(
configMinimumVisitLength: minimumVisitLength * 1000,
configHeartBeatTimer: heartbeatDelay * 1000,
callback,
activityMetrics: configuration.activityMetrics,
};
}

Expand Down Expand Up @@ -1232,6 +1320,10 @@ export function Tracker(
}

activityTrackingConfig.configurations[actionKey] = undefined;

if (!activityTrackingConfig.configurations.pagePing && !activityTrackingConfig.configurations.callback) {
resetActivityMetricsState();
}
}

const apiMethods = {
Expand Down
1 change: 1 addition & 0 deletions libraries/browser-tracker-core/src/tracker/schemata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export const WEB_PAGE_SCHEMA = 'iglu:com.snowplowanalytics.snowplow/web_page/jso
export const BROWSER_CONTEXT_SCHEMA = 'iglu:com.snowplowanalytics.snowplow/browser_context/jsonschema/2-0-0';
export const CLIENT_SESSION_SCHEMA = 'iglu:com.snowplowanalytics.snowplow/client_session/jsonschema/1-0-2';
export const APPLICATION_CONTEXT_SCHEMA = 'iglu:com.snowplowanalytics.snowplow/application/jsonschema/1-0-0';
export const ACTIVITY_METRICS_SCHEMA = 'iglu:com.snowplowanalytics.snowplow/activity_metrics/jsonschema/1-0-0';
21 changes: 21 additions & 0 deletions libraries/browser-tracker-core/src/tracker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,30 @@ export type ActivityCallbackData = {
maxXOffset: number;
/** The maximum Y scroll position for the current page view */
maxYOffset: number;
/** Activity metrics accumulated since the last ping (only when activityMetrics is enabled) */
activityMetrics?: ActivityMetrics;
};

/** The callback for enableActivityTrackingCallback */
export type ActivityCallback = (data: ActivityCallbackData) => void;

/**
* Quantitative activity metrics accumulated between page pings.
* Attached as a context entity when activityMetrics is enabled.
*/
export type ActivityMetrics = {
/** Cumulative Euclidean mouse distance in pixels from mousemove events */
mouseDistance: number;
/** Cumulative |deltaX|+|deltaY| scroll distance in pixels */
scrollDistance: number;
/** Count of keydown events */
keyPresses: number;
/** Count of click events */
clicks: number;
/** Count of touchstart events */
touches: number;
};

/**
* The base configuration for activity tracking
*/
Expand All @@ -240,6 +259,8 @@ export interface ActivityTrackingConfiguration {
minimumVisitLength: number;
/** The interval at which the callback will be fired */
heartbeatDelay: number;
/** Enable activity metrics to attach an activity_metrics entity to each page ping */
activityMetrics?: boolean;
}

/**
Expand Down
Loading
Loading