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 deep link tracking to the React Native tracker #1398

Merged
merged 2 commits into from
Dec 10, 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
@@ -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; [DeepLinkConfiguration](./react-native-tracker.deeplinkconfiguration.md) &gt; [deepLinkContext](./react-native-tracker.deeplinkconfiguration.deeplinkcontext.md)

## DeepLinkConfiguration.deepLinkContext property

Whether to track the deep link context entity with information from the deep link received event on the first screen view event.

<b>Signature:</b>

```typescript
deepLinkContext?: boolean;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- 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; [DeepLinkConfiguration](./react-native-tracker.deeplinkconfiguration.md)

## DeepLinkConfiguration interface

Configuration for deep link tracking

<b>Signature:</b>

```typescript
export interface DeepLinkConfiguration
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [deepLinkContext?](./react-native-tracker.deeplinkconfiguration.deeplinkcontext.md) | boolean | <i>(Optional)</i> Whether to track the deep link context entity with information from the deep link received event on the first screen view event. |

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
| [CoreConfiguration](./react-native-tracker.coreconfiguration.md) | The configuration object for the tracker core library |
| [CorePlugin](./react-native-tracker.coreplugin.md) | Interface which defines Core Plugins |
| [CorePluginConfiguration](./react-native-tracker.corepluginconfiguration.md) | The configuration of the plugin to add |
| [DeepLinkConfiguration](./react-native-tracker.deeplinkconfiguration.md) | Configuration for deep link tracking |
| [DeviceTimestamp](./react-native-tracker.devicetimestamp.md) | A representation of a Device Timestamp (dtm) |
| [Emitter](./react-native-tracker.emitter.md) | Emitter is responsible for sending events to the collector. It manages the event queue and sends events in batches depending on configuration. |
| [EmitterConfiguration](./react-native-tracker.emitterconfiguration.md) | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ Creates a new tracker instance with the given configuration
<b>Signature:</b>

```typescript
export declare function newTracker(configuration: TrackerConfiguration & EmitterConfiguration & SessionConfiguration & SubjectConfiguration & EventStoreConfiguration & ScreenTrackingConfiguration & PlatformContextConfiguration): Promise<ReactNativeTracker>;
export declare function newTracker(configuration: TrackerConfiguration & EmitterConfiguration & SessionConfiguration & SubjectConfiguration & EventStoreConfiguration & ScreenTrackingConfiguration & PlatformContextConfiguration & DeepLinkConfiguration): Promise<ReactNativeTracker>;
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| configuration | [TrackerConfiguration](./react-native-tracker.trackerconfiguration.md) &amp; EmitterConfiguration &amp; [SessionConfiguration](./react-native-tracker.sessionconfiguration.md) &amp; [SubjectConfiguration](./react-native-tracker.subjectconfiguration.md) &amp; [EventStoreConfiguration](./react-native-tracker.eventstoreconfiguration.md) &amp; ScreenTrackingConfiguration &amp; [PlatformContextConfiguration](./react-native-tracker.platformcontextconfiguration.md) | Configuration for the tracker |
| configuration | [TrackerConfiguration](./react-native-tracker.trackerconfiguration.md) &amp; EmitterConfiguration &amp; [SessionConfiguration](./react-native-tracker.sessionconfiguration.md) &amp; [SubjectConfiguration](./react-native-tracker.subjectconfiguration.md) &amp; [EventStoreConfiguration](./react-native-tracker.eventstoreconfiguration.md) &amp; ScreenTrackingConfiguration &amp; [PlatformContextConfiguration](./react-native-tracker.platformcontextconfiguration.md) &amp; [DeepLinkConfiguration](./react-native-tracker.deeplinkconfiguration.md) | Configuration for the tracker |

<b>Returns:</b>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export declare type ReactNativeTracker = {
readonly trackStructuredEvent: (argmap: StructuredEvent, contexts?: EventContext[]) => void;
readonly trackPageViewEvent: (argmap: PageViewEvent, contexts?: EventContext[]) => void;
readonly trackTimingEvent: (argmap: TimingProps, contexts?: EventContext[]) => void;
readonly trackDeepLinkReceivedEvent: (argmap: DeepLinkReceivedProps, contexts?: EventContext[]) => void;
readonly trackMessageNotificationEvent: (argmap: MessageNotificationProps, contexts?: EventContext[]) => void;
addGlobalContexts(contexts: Array<ConditionalContextProvider | ContextPrimitive> | Record<string, ConditionalContextProvider | ContextPrimitive>): void;
clearGlobalContexts(): void;
Expand Down Expand Up @@ -46,5 +47,5 @@ export declare type ReactNativeTracker = {
readonly refreshPlatformContext: () => Promise<void>;
};
```
<b>References:</b> [EventContext](./react-native-tracker.eventcontext.md)<!-- -->, [ScreenViewProps](./react-native-tracker.screenviewprops.md)<!-- -->, [ScrollChangedProps](./react-native-tracker.scrollchangedprops.md)<!-- -->, [ListItemViewProps](./react-native-tracker.listitemviewprops.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)
<b>References:</b> [EventContext](./react-native-tracker.eventcontext.md)<!-- -->, [ScreenViewProps](./react-native-tracker.screenviewprops.md)<!-- -->, [ScrollChangedProps](./react-native-tracker.scrollchangedprops.md)<!-- -->, [ListItemViewProps](./react-native-tracker.listitemviewprops.md)<!-- -->, [TimingProps](./react-native-tracker.timingprops.md)<!-- -->, [DeepLinkReceivedProps](./react-native-tracker.deeplinkreceivedprops.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
Expand Up @@ -53,6 +53,11 @@ export interface CorePluginConfiguration {
plugin: CorePlugin;
}

// @public
export interface DeepLinkConfiguration {
deepLinkContext?: boolean;
}

// @public
export type DeepLinkReceivedProps = {
url: string;
Expand Down Expand Up @@ -255,7 +260,7 @@ export type MessageNotificationProps = {
};

// @public
export function newTracker(configuration: TrackerConfiguration & EmitterConfiguration & SessionConfiguration & SubjectConfiguration & EventStoreConfiguration & ScreenTrackingConfiguration & PlatformContextConfiguration): Promise<ReactNativeTracker>;
export function newTracker(configuration: TrackerConfiguration & EmitterConfiguration & SessionConfiguration & SubjectConfiguration & EventStoreConfiguration & ScreenTrackingConfiguration & PlatformContextConfiguration & DeepLinkConfiguration): Promise<ReactNativeTracker>;

// @public
export interface PageViewEvent {
Expand Down Expand Up @@ -348,6 +353,7 @@ export type ReactNativeTracker = {
readonly trackStructuredEvent: (argmap: StructuredEvent, contexts?: EventContext[]) => void;
readonly trackPageViewEvent: (argmap: PageViewEvent, contexts?: EventContext[]) => void;
readonly trackTimingEvent: (argmap: TimingProps, contexts?: EventContext[]) => void;
readonly trackDeepLinkReceivedEvent: (argmap: DeepLinkReceivedProps, contexts?: EventContext[]) => void;
readonly trackMessageNotificationEvent: (argmap: MessageNotificationProps, contexts?: EventContext[]) => void;
addGlobalContexts(contexts: Array<ConditionalContextProvider | ContextPrimitive> | Record<string, ConditionalContextProvider | ContextPrimitive>): void;
clearGlobalContexts(): void;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@snowplow/react-native-tracker",
"comment": "Add deep link tracking to the React Native tracker",
"type": "none"
}
],
"packageName": "@snowplow/react-native-tracker"
}
6 changes: 6 additions & 0 deletions trackers/react-native-tracker/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
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 DEEP_LINK_RECEIVED_EVENT_SCHEMA = 'iglu:com.snowplowanalytics.mobile/deep_link_received/jsonschema/1-0-0';
export const SCREEN_VIEW_EVENT_SCHEMA = 'iglu:com.snowplowanalytics.mobile/screen_view/jsonschema/1-0-0';

export const CLIENT_SESSION_ENTITY_SCHEMA ='iglu:com.snowplowanalytics.snowplow/client_session/jsonschema/1-0-2'
export const MOBILE_CONTEXT_SCHEMA = 'iglu:com.snowplowanalytics.snowplow/mobile_context/jsonschema/1-0-3';
export const DEEP_LINK_ENTITY_SCHEMA = 'iglu:com.snowplowanalytics.mobile/deep_link/jsonschema/1-0-0';

export const PAGE_URL_PROPERTY = 'url';
export const PAGE_REFERRER_PROPERTY = 'refr';
68 changes: 68 additions & 0 deletions trackers/react-native-tracker/src/plugins/deep_links/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { buildSelfDescribingEvent, CorePluginConfiguration, PayloadBuilder, TrackerCore } from '@snowplow/tracker-core';
import { DeepLinkConfiguration, DeepLinkReceivedProps, EventContext } from '../../types';
import { DEEP_LINK_ENTITY_SCHEMA, DEEP_LINK_RECEIVED_EVENT_SCHEMA, PAGE_REFERRER_PROPERTY, PAGE_URL_PROPERTY, SCREEN_VIEW_EVENT_SCHEMA } from '../../constants';
import { getUsefulSchema } from '../../utils';

interface DeepLinksPlugin extends CorePluginConfiguration {
trackDeepLinkReceivedEvent: (argmap: DeepLinkReceivedProps, contexts?: EventContext[]) => void;
}

export function newDeepLinksPlugin(
{ deepLinkContext = true }: DeepLinkConfiguration,
core: TrackerCore
): DeepLinksPlugin {
let lastDeepLink: DeepLinkReceivedProps | undefined;

const beforeTrack = (payloadBuilder: PayloadBuilder) => {
const schema = getUsefulSchema(payloadBuilder);

if (schema == SCREEN_VIEW_EVENT_SCHEMA && lastDeepLink) {
const { url, referrer } = lastDeepLink;
if (url) {
payloadBuilder.add(PAGE_URL_PROPERTY, url);
}
if (referrer) {
payloadBuilder.add(PAGE_REFERRER_PROPERTY, referrer);
}

if (deepLinkContext) {
payloadBuilder.addContextEntity({
schema: DEEP_LINK_ENTITY_SCHEMA,
data: lastDeepLink,
});
}

// Clear the last deep link since we only add it to the first screen view event
lastDeepLink = undefined;
}
};

const trackDeepLinkReceivedEvent = (argmap: DeepLinkReceivedProps, contexts?: EventContext[]) => {
lastDeepLink = argmap;

const payload = buildSelfDescribingEvent({
event: {
schema: DEEP_LINK_RECEIVED_EVENT_SCHEMA,
data: argmap,
},
});

// Add atomic event properties
const { url, referrer } = argmap;
if (url) {
payload.add(PAGE_URL_PROPERTY, url);
}
if (referrer) {
payload.add(PAGE_REFERRER_PROPERTY, referrer);
}

core.track(payload, contexts);
};

return {
trackDeepLinkReceivedEvent,
plugin: {
beforeTrack,
},
};
}
9 changes: 8 additions & 1 deletion trackers/react-native-tracker/src/tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '@snowplow/browser-plugin-screen-tracking';

import {
DeepLinkConfiguration,
EventContext,
EventStoreConfiguration,
ListItemViewProps,
Expand All @@ -25,6 +26,7 @@ import {
TrackerConfiguration,
} from './types';
import { newSessionPlugin } from './plugins/session';
import { newDeepLinksPlugin } from './plugins/deep_links';
import { newPlugins } from './plugins';
import { newPlatformContextPlugin } from './plugins/platform_context';

Expand All @@ -42,7 +44,8 @@ export async function newTracker(
SubjectConfiguration &
EventStoreConfiguration &
ScreenTrackingConfiguration &
PlatformContextConfiguration
PlatformContextConfiguration &
DeepLinkConfiguration
): Promise<ReactNativeTracker> {
const { namespace, appId, encodeBase64 = false } = configuration;
if (configuration.eventStore === undefined) {
Expand All @@ -68,6 +71,9 @@ export async function newTracker(
const sessionPlugin = await newSessionPlugin(configuration);
addPlugin(sessionPlugin);

const deepLinksPlugin = await newDeepLinksPlugin(configuration, core);
addPlugin(deepLinksPlugin);

const subject = newSubject(core, configuration);
addPlugin(subject.subjectPlugin);

Expand Down Expand Up @@ -121,6 +127,7 @@ export async function newTracker(
},
[namespace]
),
trackDeepLinkReceivedEvent: deepLinksPlugin.trackDeepLinkReceivedEvent,
};
initializedTrackers[namespace] = { tracker, core };

Expand Down
26 changes: 18 additions & 8 deletions trackers/react-native-tracker/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,17 @@ export interface PlatformContextConfiguration {
platformContextRetriever?: PlatformContextRetriever;
}

/**
* Configuration for deep link tracking
*/
export interface DeepLinkConfiguration {
/**
* Whether to track the deep link context entity with information from the deep link received event on the first screen view event.
* @defaultValue true
*/
deepLinkContext?: boolean;
}

/**
* Configuration of subject properties tracked with events
*/
Expand Down Expand Up @@ -641,14 +652,13 @@ export type ReactNativeTracker = {
*/
readonly trackTimingEvent: (argmap: TimingProps, contexts?: EventContext[]) => void;

// TODO:
// /**
// * Tracks a deep link received event
// *
// * @param argmap - The deep link received event properties
// * @param contexts - The array of event contexts
// */
// readonly trackDeepLinkReceivedEvent: (argmap: DeepLinkReceivedProps, contexts?: EventContext[]) => void;
/**
* Tracks a deep link received event
*
* @param argmap - The deep link received event properties
* @param contexts - The array of event contexts
*/
readonly trackDeepLinkReceivedEvent: (argmap: DeepLinkReceivedProps, contexts?: EventContext[]) => void;

/**
* Tracks a message notification event
Expand Down
107 changes: 107 additions & 0 deletions trackers/react-native-tracker/test/plugins/deep_links.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
DEEP_LINK_ENTITY_SCHEMA,
DEEP_LINK_RECEIVED_EVENT_SCHEMA,
SCREEN_VIEW_EVENT_SCHEMA,
} from '../../src/constants';
import { newDeepLinksPlugin } from '../../src/plugins/deep_links';
import { buildSelfDescribingEvent, Payload, trackerCore } from '@snowplow/tracker-core';

describe('Deep Link plugin', () => {
it('adds the url and refr properties on the deep link event', () => {
const payloads: Payload[] = [];
const tracker = trackerCore({
callback: (pb) => payloads.push(pb.build()),
base64: false,
});
const deepLinksPlugin = newDeepLinksPlugin({}, tracker);
tracker.addPlugin(deepLinksPlugin);

deepLinksPlugin.trackDeepLinkReceivedEvent({
url: 'http://url.com',
referrer: 'http://referrer.com',
});

expect(payloads.length).toBe(1);
const [{ url, refr, ue_pr }] = payloads as any;
expect(url).toBe('http://url.com');
expect(refr).toBe('http://referrer.com');

const { data } = JSON.parse(ue_pr);
expect(data.schema).toBe(DEEP_LINK_RECEIVED_EVENT_SCHEMA);
expect(data.data.url).toBe('http://url.com');
expect(data.data.referrer).toBe('http://referrer.com');
});

it('adds the deep link context to the first screen view event', () => {
const payloads: Payload[] = [];
const tracker = trackerCore({
callback: (pb) => payloads.push(pb.build()),
base64: false,
});
const deepLinksPlugin = newDeepLinksPlugin({}, tracker);
tracker.addPlugin(deepLinksPlugin);

deepLinksPlugin.trackDeepLinkReceivedEvent({
url: 'http://url.com',
referrer: 'http://referrer.com',
});
tracker.track(
buildSelfDescribingEvent({
event: {
schema: SCREEN_VIEW_EVENT_SCHEMA,
data: {},
},
})
);
tracker.track(
buildSelfDescribingEvent({
event: {
schema: SCREEN_VIEW_EVENT_SCHEMA,
data: {},
},
})
);

expect(payloads.length).toBe(3);
const [, { url, refr, co }, { co: co2 }] = payloads as any;
expect(url).toBe('http://url.com');
expect(refr).toBe('http://referrer.com');
expect(co).not.toBeUndefined();
const entities = JSON.parse(co).data;
const deepLinkEntity = entities.find((entity: any) => entity.schema === DEEP_LINK_ENTITY_SCHEMA);
expect(deepLinkEntity).not.toBeUndefined();
expect(deepLinkEntity.data.url).toBe('http://url.com');
expect(deepLinkEntity.data.referrer).toBe('http://referrer.com');

expect(co2 ?? '').not.toContain(DEEP_LINK_ENTITY_SCHEMA);
});

it('does not add the deep link entity if disabled', () => {
const payloads: Payload[] = [];
const tracker = trackerCore({
callback: (pb) => payloads.push(pb.build()),
base64: false,
});
const deepLinksPlugin = newDeepLinksPlugin({ deepLinkContext: false }, tracker);
tracker.addPlugin(deepLinksPlugin);

deepLinksPlugin.trackDeepLinkReceivedEvent({
url: 'http://url.com',
referrer: 'http://referrer.com',
});
tracker.track(
buildSelfDescribingEvent({
event: {
schema: SCREEN_VIEW_EVENT_SCHEMA,
data: {},
},
})
);

expect(payloads.length).toBe(2);
const [, { url, refr, co }] = payloads as any;
expect(url).toBe('http://url.com');
expect(refr).toBe('http://referrer.com');
expect(co ?? '').not.toContain(DEEP_LINK_ENTITY_SCHEMA);
});
});
Loading