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

Add internal session plugin to the React Native tracker #1388

Merged
merged 6 commits into from
Nov 28, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
| [PayloadBuilder](./react-native-tracker.payloadbuilder.md) | Interface for mutable object encapsulating tracker payload |
| [RuleSet](./react-native-tracker.ruleset.md) | A ruleset has accept or reject properties that contain rules for matching Iglu schema URIs |
| [SessionConfiguration](./react-native-tracker.sessionconfiguration.md) | Configuration for session tracking |
| [SessionState](./react-native-tracker.sessionstate.md) | Current session state that is tracked in events. |
| [StructuredEvent](./react-native-tracker.structuredevent.md) | A Structured Event A classic style of event tracking, allows for easier movement between analytics systems. A loosely typed event, creating a Self Describing event is preferred, but useful for interoperability. |
| [SubjectConfiguration](./react-native-tracker.subjectconfiguration.md) | Configuration of subject properties tracked with events |
| [TrackerConfiguration](./react-native-tracker.trackerconfiguration.md) | The configuration object for initialising the tracker |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ export declare type ReactNativeTracker = {
readonly setScreenViewport: (newView: ScreenSize) => void;
readonly setColorDepth: (newLang: number) => void;
readonly setSubjectData: (config: SubjectConfiguration) => void;
readonly getSessionUserId: () => Promise<string | undefined>;
readonly getSessionId: () => Promise<string | undefined>;
readonly getSessionIndex: () => Promise<number | undefined>;
readonly getSessionState: () => Promise<SessionState | undefined>;
};
```
<b>References:</b> [EventContext](./react-native-tracker.eventcontext.md)<!-- -->, [TimingProps](./react-native-tracker.timingprops.md)<!-- -->, [MessageNotificationProps](./react-native-tracker.messagenotificationprops.md)<!-- -->, [ScreenSize](./react-native-tracker.screensize.md)<!-- -->, [SubjectConfiguration](./react-native-tracker.subjectconfiguration.md)
<b>References:</b> [EventContext](./react-native-tracker.eventcontext.md)<!-- -->, [TimingProps](./react-native-tracker.timingprops.md)<!-- -->, [MessageNotificationProps](./react-native-tracker.messagenotificationprops.md)<!-- -->, [ScreenSize](./react-native-tracker.screensize.md)<!-- -->, [SubjectConfiguration](./react-native-tracker.subjectconfiguration.md)<!-- -->, [SessionState](./react-native-tracker.sessionstate.md)

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@snowplow/react-native-tracker](./react-native-tracker.md) &gt; [SessionState](./react-native-tracker.sessionstate.md) &gt; [eventIndex](./react-native-tracker.sessionstate.eventindex.md)

## SessionState.eventIndex property

Optional index of the current event in the session

<b>Signature:</b>

```typescript
eventIndex?: number;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@snowplow/react-native-tracker](./react-native-tracker.md) &gt; [SessionState](./react-native-tracker.sessionstate.md) &gt; [firstEventId](./react-native-tracker.sessionstate.firsteventid.md)

## SessionState.firstEventId property

The optional identifier of the first event for this session

<b>Signature:</b>

```typescript
firstEventId?: string;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@snowplow/react-native-tracker](./react-native-tracker.md) &gt; [SessionState](./react-native-tracker.sessionstate.md) &gt; [firstEventTimestamp](./react-native-tracker.sessionstate.firsteventtimestamp.md)

## SessionState.firstEventTimestamp property

Optional date-time timestamp of when the first event in the session was tracked

<b>Signature:</b>

```typescript
firstEventTimestamp?: string;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@snowplow/react-native-tracker](./react-native-tracker.md) &gt; [SessionState](./react-native-tracker.sessionstate.md)

## SessionState interface

Current session state that is tracked in events.

<b>Signature:</b>

```typescript
export interface SessionState
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [eventIndex?](./react-native-tracker.sessionstate.eventindex.md) | number | <i>(Optional)</i> Optional index of the current event in the session |
| [firstEventId?](./react-native-tracker.sessionstate.firsteventid.md) | string | <i>(Optional)</i> The optional identifier of the first event for this session |
| [firstEventTimestamp?](./react-native-tracker.sessionstate.firsteventtimestamp.md) | string | <i>(Optional)</i> Optional date-time timestamp of when the first event in the session was tracked |
| [previousSessionId?](./react-native-tracker.sessionstate.previoussessionid.md) | string | <i>(Optional)</i> The previous session identifier for this user |
| [sessionId](./react-native-tracker.sessionstate.sessionid.md) | string | An identifier for the session |
| [sessionIndex](./react-native-tracker.sessionstate.sessionindex.md) | number | The index of the current session for this user |
| [storageMechanism](./react-native-tracker.sessionstate.storagemechanism.md) | string | The mechanism that the session information has been stored on the device |
| [userId](./react-native-tracker.sessionstate.userid.md) | string | An identifier for the user of the session |

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@snowplow/react-native-tracker](./react-native-tracker.md) &gt; [SessionState](./react-native-tracker.sessionstate.md) &gt; [previousSessionId](./react-native-tracker.sessionstate.previoussessionid.md)

## SessionState.previousSessionId property

The previous session identifier for this user

<b>Signature:</b>

```typescript
previousSessionId?: string;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@snowplow/react-native-tracker](./react-native-tracker.md) &gt; [SessionState](./react-native-tracker.sessionstate.md) &gt; [sessionId](./react-native-tracker.sessionstate.sessionid.md)

## SessionState.sessionId property

An identifier for the session

<b>Signature:</b>

```typescript
sessionId: string;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@snowplow/react-native-tracker](./react-native-tracker.md) &gt; [SessionState](./react-native-tracker.sessionstate.md) &gt; [sessionIndex](./react-native-tracker.sessionstate.sessionindex.md)

## SessionState.sessionIndex property

The index of the current session for this user

<b>Signature:</b>

```typescript
sessionIndex: number;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@snowplow/react-native-tracker](./react-native-tracker.md) &gt; [SessionState](./react-native-tracker.sessionstate.md) &gt; [storageMechanism](./react-native-tracker.sessionstate.storagemechanism.md)

## SessionState.storageMechanism property

The mechanism that the session information has been stored on the device

<b>Signature:</b>

```typescript
storageMechanism: string;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@snowplow/react-native-tracker](./react-native-tracker.md) &gt; [SessionState](./react-native-tracker.sessionstate.md) &gt; [userId](./react-native-tracker.sessionstate.userid.md)

## SessionState.userId property

An identifier for the user of the session

<b>Signature:</b>

```typescript
userId: string;
```
16 changes: 16 additions & 0 deletions api-docs/docs/react-native-tracker/react-native-tracker.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,10 @@ export type ReactNativeTracker = {
readonly setScreenViewport: (newView: ScreenSize) => void;
readonly setColorDepth: (newLang: number) => void;
readonly setSubjectData: (config: SubjectConfiguration) => void;
readonly getSessionUserId: () => Promise<string | undefined>;
readonly getSessionId: () => Promise<string | undefined>;
readonly getSessionIndex: () => Promise<number | undefined>;
readonly getSessionState: () => Promise<SessionState | undefined>;
};

// @public
Expand Down Expand Up @@ -366,6 +370,18 @@ export interface SessionConfiguration {
foregroundSessionTimeout?: number;
}

// @public
export interface SessionState {
eventIndex?: number;
firstEventId?: string;
firstEventTimestamp?: string;
previousSessionId?: string;
sessionId: string;
sessionIndex: number;
storageMechanism: string;
userId: string;
}

// @public
export interface StructuredEvent {
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@snowplow/react-native-tracker",
"comment": "Add internal session plugin to the React Native tracker (#1388)",
"type": "none"
}
],
"packageName": "@snowplow/react-native-tracker"
}
4 changes: 2 additions & 2 deletions common/config/rush/browser-approved-packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@
},
{
"name": "@types/uuid",
"allowedCategories": [ "libraries", "plugins" ]
"allowedCategories": [ "libraries", "plugins", "trackers" ]
},
{
"name": "@types/vimeo__player",
Expand Down Expand Up @@ -440,7 +440,7 @@
},
{
"name": "uuid",
"allowedCategories": [ "libraries", "plugins" ]
"allowedCategories": [ "libraries", "plugins", "trackers" ]
},
{
"name": "wdio-chromedriver-service",
Expand Down
6 changes: 6 additions & 0 deletions common/config/rush/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion common/config/rush/repo-state.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
"pnpmShrinkwrapHash": "f5c19c955ef1b13843dbacdb20bdd6461a9a5042",
"pnpmShrinkwrapHash": "bf1ad132a0781c6f74cda6742bc190f7802256f1",
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
}
4 changes: 3 additions & 1 deletion trackers/react-native-tracker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"@snowplow/tracker-core": "workspace:*",
"@react-native-async-storage/async-storage": "~2.0.0",
"react-native-get-random-values": "~1.11.0",
"tslib": "^2.3.1"
"tslib": "^2.3.1",
"uuid": "^10.0.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "~5.15.0",
Expand All @@ -59,6 +60,7 @@
"typescript": "~4.6.2",
"@types/jest": "~28.1.1",
"@types/node": "~14.6.0",
"@types/uuid": "^10.0.0",
"jest": "~28.1.3",
"react": "18.2.0",
"ts-jest": "~28.0.8",
Expand Down
4 changes: 4 additions & 0 deletions trackers/react-native-tracker/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const FOREGROUND_EVENT_SCHEMA = 'iglu:com.snowplowanalytics.snowplow/application_foreground/jsonschema/1-0-0';
export const BACKGROUND_EVENT_SCHEMA = 'iglu:com.snowplowanalytics.snowplow/application_background/jsonschema/1-0-0';

export const CLIENT_SESSION_ENTITY_SCHEMA ='iglu:com.snowplowanalytics.snowplow/client_session/jsonschema/1-0-2'
124 changes: 124 additions & 0 deletions trackers/react-native-tracker/src/plugins/session/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { CorePluginConfiguration, PayloadBuilder } from '@snowplow/tracker-core';
import { SessionConfiguration, SessionState, TrackerConfiguration } from '../../types';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { v4 as uuidv4 } from 'uuid';
import { BACKGROUND_EVENT_SCHEMA, CLIENT_SESSION_ENTITY_SCHEMA, FOREGROUND_EVENT_SCHEMA } from '../../constants';
import { getUsefulSchema } from '../../utils';

interface StoredSessionState {
userId: string;
sessionId: string;
sessionIndex: number;
}

interface SessionPlugin extends CorePluginConfiguration {
getSessionUserId: () => Promise<string | undefined>;
getSessionId: () => Promise<string | undefined>;
getSessionIndex: () => Promise<number | undefined>;
getSessionState: () => Promise<SessionState>;
startNewSession: () => Promise<void>;
}

async function storeSessionState(namespace: string, state: StoredSessionState) {
const { userId, sessionId, sessionIndex } = state;
await AsyncStorage.setItem(`snowplow_${namespace}_session`, JSON.stringify({ userId, sessionId, sessionIndex }));
}

async function resumeStoredSession(namespace: string): Promise<SessionState> {
const storedState = await AsyncStorage.getItem(`snowplow_${namespace}_session`);
if (storedState) {
const state = JSON.parse(storedState) as StoredSessionState;
return {
userId: state.userId,
sessionId: uuidv4(),
previousSessionId: state.sessionId,
sessionIndex: state.sessionIndex + 1,
storageMechanism: 'LOCAL_STORAGE',
};
} else {
return {
userId: uuidv4(),
sessionId: uuidv4(),
sessionIndex: 1,
storageMechanism: 'LOCAL_STORAGE',
};
}
}

/**
* Creates a new session plugin for tracking the session information.
* The plugin will add the session context to all events and start a new session if the current one has timed out.
*
* The session state is stored in AsyncStorage.
* Each restart of the app or creation of a new tracker instance will trigger a new session with reference to the previous session.
*/
export async function newSessionPlugin({
namespace,
foregroundSessionTimeout,
backgroundSessionTimeout,
}: TrackerConfiguration & SessionConfiguration): Promise<SessionPlugin> {
let sessionState = await resumeStoredSession(namespace);
await storeSessionState(namespace, sessionState);

let inBackground = false;
let lastUpdateTs = new Date().getTime();

const startNewSession = async () => {
sessionState = {
userId: sessionState.userId,
storageMechanism: sessionState.storageMechanism,
sessionId: uuidv4(),
sessionIndex: sessionState.sessionIndex + 1,
previousSessionId: sessionState.sessionId,
};
};

const getTimeoutMs = () => {
return ((inBackground ? backgroundSessionTimeout : foregroundSessionTimeout) ?? 30 * 60) * 1000;
};

const beforeTrack = (payloadBuilder: PayloadBuilder) => {
// check if session has timed out and start a new one if necessary
const now = new Date();
const timeDiff = now.getTime() - lastUpdateTs;
if (timeDiff > getTimeoutMs()) {
startNewSession();
storeSessionState(namespace, sessionState);
}
lastUpdateTs = now.getTime();

// update event properties
sessionState.eventIndex = (sessionState.eventIndex ?? 0) + 1;
if (sessionState.eventIndex === 1) {
sessionState.firstEventId = payloadBuilder.getPayload().eid as string;
sessionState.firstEventTimestamp = now.toISOString();
}

// update background state
if (payloadBuilder.getPayload().e === 'ue') {
const schema = getUsefulSchema(payloadBuilder);
if (schema === FOREGROUND_EVENT_SCHEMA) {
inBackground = false;
} else if (schema === BACKGROUND_EVENT_SCHEMA) {
inBackground = true;
}
}

// add session context to the payload
payloadBuilder.addContextEntity({
schema: CLIENT_SESSION_ENTITY_SCHEMA,
data: { ...sessionState },
});
};

return {
getSessionUserId: () => Promise.resolve(sessionState.userId),
getSessionId: () => Promise.resolve(sessionState.sessionId),
getSessionIndex: () => Promise.resolve(sessionState.sessionIndex),
getSessionState: () => Promise.resolve(sessionState),
startNewSession,
plugin: {
beforeTrack,
},
};
}
Loading
Loading