Skip to content

Commit

Permalink
feat: add session activity system (#1918)
Browse files Browse the repository at this point in the history
  • Loading branch information
cajames authored Jun 27, 2024
1 parent 8add4cb commit d4aa5e2
Show file tree
Hide file tree
Showing 17 changed files with 683 additions and 272 deletions.
46 changes: 46 additions & 0 deletions packages/internal/metrics/src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { errorBoundary } from './utils/errorBoundary';
import { track, TrackProperties } from './track';

type ErrorEventProperties =
| TrackProperties & {
isTrackError?: never;
errorMessage?: never;
errorStack?: never;
};

const trackErrorFn = (
moduleName: string,
eventName: string,
error: Error,
properties?: ErrorEventProperties,
) => {
const { message } = error;
let stack = error.stack || '';
const { cause } = error;

if (cause instanceof Error) {
stack = `${stack} \nCause: ${cause.message}\n ${cause.stack}`;
}

track(moduleName, `trackError_${eventName}`, {
...(properties || {}),
errorMessage: message,
errorStack: stack,
isTrackError: true,
});
};

/**
* Track an event and it's performance. Works similarly to `track`, but also includes a duration.
* @param moduleName Name of the module being tracked (for namespacing purposes), e.g. `passport`
* @param eventName Name of the event, e.g. `clickItem`
* @param error Error object to be tracked
* @param properties Other properties to be sent with the event, other than duration
*
* @example
* ```ts
* trackError("passport", "sendTransactionFailed", error);
* trackError("passport", "getItemFailed", error, { otherProperty: "value" });
* ```
*/
export const trackError = errorBoundary(trackErrorFn);
137 changes: 137 additions & 0 deletions packages/internal/metrics/src/flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { AllowedTrackProperties, TrackProperties } from './track';
import { errorBoundary } from './utils/errorBoundary';
import { generateFlowId } from './utils/id';
import { trackDuration } from './performance';

export type Flow = {
details: {
moduleName: string;
flowName: string;
flowId: string;
flowStartTime: number;
};
/**
* Track an event in the flow
* @param eventName Name of the event
* @param properties Object containing event properties
*/
addEvent: (eventName: string, properties?: AllowedTrackProperties) => void;
/**
* Function to add new flow properties
* @param newProperties Object new properties
*/
addFlowProperties: (properties: AllowedTrackProperties) => void;
};

// Flow Tracking Functions
// -----------------------------------
// Write a function to take multiple objects as arguments, and merge them into one object
const mergeProperties = (
...args: (TrackProperties | undefined)[]
): TrackProperties => {
const hasProperties = args.some((arg) => !!arg);
if (!hasProperties) {
return {};
}
let finalProperties: Record<string, any> = {};
args.forEach((arg) => {
if (arg) {
finalProperties = {
...finalProperties,
...arg,
};
}
});

return finalProperties;
};

const cleanEventName = (eventName: string) => eventName.replace(/[^a-zA-Z0-9\s\-_]/g, '');
const getEventName = (flowName: string, eventName: string) => `${flowName}_${cleanEventName(eventName)}`;

const trackFlowFn = (
moduleName: string,
flowName: string,
properties?: AllowedTrackProperties,
): Flow => {
// Track the start of the flow
const flowId = generateFlowId();
const flowStartTime = Date.now();

// Flow tracking
let currentStepCount = 0;
let previousStepTime = 0;

let flowProperties: TrackProperties = {};
const mergeFlowProps = (...args: (TrackProperties | undefined)[]) => mergeProperties(flowProperties, ...args, {
flowId,
flowName,
});

// Set up flow properties
flowProperties = mergeFlowProps(properties);

const addFlowProperties = (newProperties: AllowedTrackProperties) => {
if (newProperties) {
flowProperties = mergeFlowProps(newProperties);
}
};

const addEvent = (
eventName: string,
eventProperties?: AllowedTrackProperties,
) => {
const event = getEventName(flowName, eventName);

// Calculate duration since previous step
let duration = 0;
const currentTime = performance.now();
if (currentStepCount > 0) {
duration = currentTime - previousStepTime;
}
const mergedProps = mergeFlowProps(eventProperties, {
flowEventName: eventName,
flowStep: currentStepCount,
});
trackDuration(moduleName, event, duration, mergedProps);

// Increment counters
currentStepCount++;
previousStepTime = currentTime;
};

// Trigger a Start Event as a record of creating the flow
addEvent('Start');

return {
details: {
moduleName,
flowName,
flowId,
flowStartTime,
},
addEvent: errorBoundary(addEvent),
addFlowProperties: errorBoundary(addFlowProperties),
};
};
/**
* Track a flow of events, including the start and end of the flow.
* Works similarly to `track`
* @param moduleName Name of the module being tracked (for namespacing purposes), e.g. `passport`
* @param flowName Name of the flow, e.g. `performTransaction`
* @param properties Other properties to be sent with the event, other than duration
*
* @example
* ```ts
* const flow = trackFlow("passport", "performTransaction", { transationType: "transfer" });
* // Do something...
* flow.addEvent("clickItem");
* // Do something...
* flow.addFlowProperties({ item: "item1" });
* flow.addEvent("guardianCheck", {"invisible": "true"});
* // Do something...
* flow.addEvent("guardianCheckComplete");
* flow.end();
* ```
*/
export const trackFlow = errorBoundary(trackFlowFn);
10 changes: 9 additions & 1 deletion packages/internal/metrics/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
// Exporting utils
import * as localStorage from './utils/localStorage';

export { track } from './track';
export { trackDuration, trackFlow, Flow } from './performance';
export { trackDuration } from './performance';
export { Flow, trackFlow } from './flow';
export { trackError } from './error';
export { identify } from './identify';
export {
setEnvironment,
Expand All @@ -8,3 +13,6 @@ export {
getDetail,
Detail,
} from './details';
export const utils = {
localStorage,
};
2 changes: 2 additions & 0 deletions packages/internal/metrics/src/initialise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,14 @@ export const initialise = async () => {
try {
const runtimeDetails = flattenProperties(getRuntimeDetails());
const existingRuntimeId = getDetail(Detail.RUNTIME_ID);
const existingIdentity = getDetail(Detail.IDENTITY);

const body = {
version: 1,
data: {
runtimeDetails,
runtimeId: existingRuntimeId,
uId: existingIdentity,
},
};
const response = await post<InitialiseResponse>('/v1/sdk/initialise', body);
Expand Down
148 changes: 3 additions & 145 deletions packages/internal/metrics/src/performance.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,4 @@
import { errorBoundary } from './utils/errorBoundary';
import { track, TrackProperties } from './track';

type PerformanceEventProperties =
| (TrackProperties & {
duration?: never;
});

export type Flow = {
details: {
moduleName: string;
flowName: string;
flowId: string;
flowStartTime: number;
};
addEvent: (eventName: string, properties?: PerformanceEventProperties) => void;
addFlowProperties: (properties: PerformanceEventProperties) => void;
end: (endProperties?: PerformanceEventProperties) => void;
};
import { track, AllowedTrackProperties } from './track';

/**
* Track an event and it's performance. Works similarly to `track`, but also includes a duration.
Expand All @@ -35,132 +17,8 @@ export const trackDuration = (
moduleName: string,
eventName: string,
duration: number,
properties?: PerformanceEventProperties,
properties?: AllowedTrackProperties,
) => track(moduleName, eventName, {
...(properties || {}),
duration,
duration: Math.round(duration),
});

// Time Tracking Functions
// -----------------------------------

// Write a function to take multiple objects as arguments, and merge them into one object
const mergeProperties = (...args: (Record<string, any> | undefined)[]) => {
const hasProperties = args.some((arg) => !!arg);
if (!hasProperties) {
return undefined;
}
let finalProperties: Record<string, any> = {};
args.forEach((arg) => {
if (arg) {
finalProperties = {
...finalProperties,
...arg,
};
}
});

return finalProperties;
};

const getEventName = (flowName: string, eventName: string) => `${flowName}_${eventName}`;

// Generate a random uuid
const generateFlowId = () => {
const s4 = () => Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
};

type FlowEventProperties = PerformanceEventProperties & {
flowId?: never;
flowStartTime?: never;
};

const trackFlowFn = (
moduleName: string,
flowName: string,
properties?: FlowEventProperties,
): Flow => {
// Track the start of the flow
const flowStartEventName = getEventName(flowName, 'start');
const flowId = generateFlowId();
const startTime = performance.now();
const flowStartTime = Math.round(startTime + performance.timeOrigin);

let flowProperties = mergeProperties(properties, {
flowId,
flowStartTime,
}) as FlowEventProperties;
trackDuration(moduleName, flowStartEventName, 0, flowProperties);

const addFlowProperties = (newProperties: FlowEventProperties) => {
flowProperties = mergeProperties(flowProperties, newProperties, {
flowId,
flowStartTime,
}) as FlowEventProperties;
};

const addEvent = (
eventName: string,
eventProperties?: FlowEventProperties,
) => {
const event = getEventName(flowName, eventName);

// Calculate time since start
const duration = Math.round(performance.now() - startTime);
// Always send the details of the startFlow props with all events in the flow
const mergedProps = mergeProperties(flowProperties, eventProperties, {
flowId,
flowStartTime,
duration,
}) as FlowEventProperties;
trackDuration(moduleName, event, duration, mergedProps);
};

const end = (endProperties?: FlowEventProperties) => {
// Track the end of the flow
const flowEndEventName = getEventName(flowName, 'end');
const duration = Math.round(performance.now() - startTime);
const mergedProps = mergeProperties(flowProperties, endProperties, {
flowId,
flowStartTime,
}) as FlowEventProperties;
trackDuration(moduleName, flowEndEventName, duration, mergedProps);
};

return {
details: {
moduleName,
flowName,
flowId,
flowStartTime,
},
addEvent: errorBoundary(addEvent),
addFlowProperties: errorBoundary(addFlowProperties),
end: errorBoundary(end),
};
};

/**
* Track a flow of events, including the start and end of the flow.
* Works similarly to `track`
* @param moduleName Name of the module being tracked (for namespacing purposes), e.g. `passport`
* @param flowName Name of the flow, e.g. `performTransaction`
* @param properties Other properties to be sent with the event, other than duration
*
* @example
* ```ts
* const flow = trackFlow("passport", "performTransaction", { transationType: "transfer" });
* // Do something...
* flow.addEvent("clickItem");
* // Do something...
* flow.addFlowProperties({ item: "item1" });
* flow.addEvent("guardianCheck", {"invisible": "true"});
* // Do something...
* flow.addEvent("guardianCheckComplete");
* flow.end();
* ```
*/
export const trackFlow = errorBoundary(trackFlowFn);
Loading

0 comments on commit d4aa5e2

Please sign in to comment.