diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.appbuild.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.appbuild.md
new file mode 100644
index 000000000..e9dc7a44f
--- /dev/null
+++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.appbuild.md
@@ -0,0 +1,15 @@
+
+
+[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [AppLifecycleConfiguration](./react-native-tracker.applifecycleconfiguration.md) > [appBuild](./react-native-tracker.applifecycleconfiguration.appbuild.md)
+
+## AppLifecycleConfiguration.appBuild property
+
+Build name of the application e.g s9f2k2d or 1.1.0 beta
+
+Entity schema: `iglu:com.snowplowanalytics.mobile/application/jsonschema/1-0-0`
+
+Signature:
+
+```typescript
+appBuild?: string;
+```
diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.appversion.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.appversion.md
new file mode 100644
index 000000000..8b5f4fdfe
--- /dev/null
+++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.appversion.md
@@ -0,0 +1,15 @@
+
+
+[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [AppLifecycleConfiguration](./react-native-tracker.applifecycleconfiguration.md) > [appVersion](./react-native-tracker.applifecycleconfiguration.appversion.md)
+
+## AppLifecycleConfiguration.appVersion property
+
+Version number of the application e.g 1.1.0 (semver or git commit hash).
+
+Entity schema if `appBuild` property is set: `iglu:com.snowplowanalytics.mobile/application/jsonschema/1-0-0` Entity schema if `appBuild` property is not set: `iglu:com.snowplowanalytics.snowplow/application/jsonschema/1-0-0`
+
+Signature:
+
+```typescript
+appVersion?: string;
+```
diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.installautotracking.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.installautotracking.md
new file mode 100644
index 000000000..707da58ae
--- /dev/null
+++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.installautotracking.md
@@ -0,0 +1,15 @@
+
+
+[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [AppLifecycleConfiguration](./react-native-tracker.applifecycleconfiguration.md) > [installAutotracking](./react-native-tracker.applifecycleconfiguration.installautotracking.md)
+
+## AppLifecycleConfiguration.installAutotracking property
+
+Whether to automatically track app install event on first run.
+
+Schema: `iglu:com.snowplowanalytics.mobile/application_install/jsonschema/1-0-0`
+
+Signature:
+
+```typescript
+installAutotracking?: boolean;
+```
diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.lifecycleautotracking.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.lifecycleautotracking.md
new file mode 100644
index 000000000..cb90cb23f
--- /dev/null
+++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.lifecycleautotracking.md
@@ -0,0 +1,15 @@
+
+
+[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [AppLifecycleConfiguration](./react-native-tracker.applifecycleconfiguration.md) > [lifecycleAutotracking](./react-native-tracker.applifecycleconfiguration.lifecycleautotracking.md)
+
+## AppLifecycleConfiguration.lifecycleAutotracking property
+
+Whether to automatically track app lifecycle events (app foreground and background events). Also adds a lifecycle context entity to all events.
+
+Foreground event schema: `iglu:com.snowplowanalytics.snowplow/application_foreground/jsonschema/1-0-0` Background event schema: `iglu:com.snowplowanalytics.snowplow/application_background/jsonschema/1-0-0` Context entity schema: `iglu:com.snowplowanalytics.mobile/application_lifecycle/jsonschema/1-0-0`
+
+Signature:
+
+```typescript
+lifecycleAutotracking?: boolean;
+```
diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.md
new file mode 100644
index 000000000..0c1f62a04
--- /dev/null
+++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.applifecycleconfiguration.md
@@ -0,0 +1,23 @@
+
+
+[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [AppLifecycleConfiguration](./react-native-tracker.applifecycleconfiguration.md)
+
+## AppLifecycleConfiguration interface
+
+Configuration for app lifecycle tracking
+
+Signature:
+
+```typescript
+export interface AppLifecycleConfiguration
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [appBuild?](./react-native-tracker.applifecycleconfiguration.appbuild.md) | string | (Optional) Build name of the application e.g s9f2k2d or 1.1.0 betaEntity schema: iglu:com.snowplowanalytics.mobile/application/jsonschema/1-0-0
|
+| [appVersion?](./react-native-tracker.applifecycleconfiguration.appversion.md) | string | (Optional) Version number of the application e.g 1.1.0 (semver or git commit hash).Entity schema if appBuild
property is set: iglu:com.snowplowanalytics.mobile/application/jsonschema/1-0-0
Entity schema if appBuild
property is not set: iglu:com.snowplowanalytics.snowplow/application/jsonschema/1-0-0
|
+| [installAutotracking?](./react-native-tracker.applifecycleconfiguration.installautotracking.md) | boolean | (Optional) Whether to automatically track app install event on first run.Schema: iglu:com.snowplowanalytics.mobile/application_install/jsonschema/1-0-0
|
+| [lifecycleAutotracking?](./react-native-tracker.applifecycleconfiguration.lifecycleautotracking.md) | boolean | (Optional) Whether to automatically track app lifecycle events (app foreground and background events). Also adds a lifecycle context entity to all events.Foreground event schema: iglu:com.snowplowanalytics.snowplow/application_foreground/jsonschema/1-0-0
Background event schema: iglu:com.snowplowanalytics.snowplow/application_background/jsonschema/1-0-0
Context entity schema: iglu:com.snowplowanalytics.mobile/application_lifecycle/jsonschema/1-0-0
|
+
diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.md
index 4f68312df..a4a06599b 100644
--- a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.md
+++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.md
@@ -24,6 +24,7 @@
| Interface | Description |
| --- | --- |
+| [AppLifecycleConfiguration](./react-native-tracker.applifecycleconfiguration.md) | Configuration for app lifecycle tracking |
| [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 |
diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.newtracker.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.newtracker.md
index a4b953deb..270a0f7f7 100644
--- a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.newtracker.md
+++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.newtracker.md
@@ -9,14 +9,14 @@ Creates a new tracker instance with the given configuration
Signature:
```typescript
-export declare function newTracker(configuration: TrackerConfiguration & EmitterConfiguration & SessionConfiguration & SubjectConfiguration & EventStoreConfiguration & ScreenTrackingConfiguration & PlatformContextConfiguration & DeepLinkConfiguration): Promise;
+export declare function newTracker(configuration: TrackerConfiguration & EmitterConfiguration & SessionConfiguration & SubjectConfiguration & EventStoreConfiguration & ScreenTrackingConfiguration & PlatformContextConfiguration & DeepLinkConfiguration & AppLifecycleConfiguration): Promise;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
-| configuration | [TrackerConfiguration](./react-native-tracker.trackerconfiguration.md) & EmitterConfiguration & [SessionConfiguration](./react-native-tracker.sessionconfiguration.md) & [SubjectConfiguration](./react-native-tracker.subjectconfiguration.md) & [EventStoreConfiguration](./react-native-tracker.eventstoreconfiguration.md) & ScreenTrackingConfiguration & [PlatformContextConfiguration](./react-native-tracker.platformcontextconfiguration.md) & [DeepLinkConfiguration](./react-native-tracker.deeplinkconfiguration.md) | Configuration for the tracker |
+| configuration | [TrackerConfiguration](./react-native-tracker.trackerconfiguration.md) & EmitterConfiguration & [SessionConfiguration](./react-native-tracker.sessionconfiguration.md) & [SubjectConfiguration](./react-native-tracker.subjectconfiguration.md) & [EventStoreConfiguration](./react-native-tracker.eventstoreconfiguration.md) & ScreenTrackingConfiguration & [PlatformContextConfiguration](./react-native-tracker.platformcontextconfiguration.md) & [DeepLinkConfiguration](./react-native-tracker.deeplinkconfiguration.md) & [AppLifecycleConfiguration](./react-native-tracker.applifecycleconfiguration.md) | Configuration for the tracker |
Returns:
diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.reactnativetracker.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.reactnativetracker.md
index 5785f9ad5..720dc9ee8 100644
--- a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.reactnativetracker.md
+++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.reactnativetracker.md
@@ -42,6 +42,9 @@ export declare type ReactNativeTracker = {
readonly getSessionId: () => Promise;
readonly getSessionIndex: () => Promise;
readonly getSessionState: () => Promise;
+ readonly getIsInBackground: () => boolean | undefined;
+ readonly getBackgroundIndex: () => number | undefined;
+ readonly getForegroundIndex: () => number | undefined;
readonly enablePlatformContext: () => Promise;
readonly disablePlatformContext: () => void;
readonly refreshPlatformContext: () => Promise;
diff --git a/api-docs/docs/react-native-tracker/react-native-tracker.api.md b/api-docs/docs/react-native-tracker/react-native-tracker.api.md
index f2f785ae1..452ad94c4 100644
--- a/api-docs/docs/react-native-tracker/react-native-tracker.api.md
+++ b/api-docs/docs/react-native-tracker/react-native-tracker.api.md
@@ -8,6 +8,14 @@ import { BrowserPlugin } from '@snowplow/browser-tracker-core';
import { BrowserPluginConfiguration } from '@snowplow/browser-tracker-core';
import { ScreenTrackingConfiguration } from '@snowplow/browser-plugin-screen-tracking';
+// @public
+export interface AppLifecycleConfiguration {
+ appBuild?: string;
+ appVersion?: string;
+ installAutotracking?: boolean;
+ lifecycleAutotracking?: boolean;
+}
+
// @public
export type ConditionalContextProvider = FilterProvider | RuleSetProvider;
@@ -260,7 +268,7 @@ export type MessageNotificationProps = {
};
// @public
-export function newTracker(configuration: TrackerConfiguration & EmitterConfiguration & SessionConfiguration & SubjectConfiguration & EventStoreConfiguration & ScreenTrackingConfiguration & PlatformContextConfiguration & DeepLinkConfiguration): Promise;
+export function newTracker(configuration: TrackerConfiguration & EmitterConfiguration & SessionConfiguration & SubjectConfiguration & EventStoreConfiguration & ScreenTrackingConfiguration & PlatformContextConfiguration & DeepLinkConfiguration & AppLifecycleConfiguration): Promise;
// @public
export interface PageViewEvent {
@@ -377,6 +385,9 @@ export type ReactNativeTracker = {
readonly getSessionId: () => Promise;
readonly getSessionIndex: () => Promise;
readonly getSessionState: () => Promise;
+ readonly getIsInBackground: () => boolean | undefined;
+ readonly getBackgroundIndex: () => number | undefined;
+ readonly getForegroundIndex: () => number | undefined;
readonly enablePlatformContext: () => Promise;
readonly disablePlatformContext: () => void;
readonly refreshPlatformContext: () => Promise;
diff --git a/common/changes/@snowplow/react-native-tracker/issue-react-native-lifecycle-tracking_2024-12-02-08-40.json b/common/changes/@snowplow/react-native-tracker/issue-react-native-lifecycle-tracking_2024-12-02-08-40.json
new file mode 100644
index 000000000..6ff5b3a87
--- /dev/null
+++ b/common/changes/@snowplow/react-native-tracker/issue-react-native-lifecycle-tracking_2024-12-02-08-40.json
@@ -0,0 +1,10 @@
+{
+ "changes": [
+ {
+ "packageName": "@snowplow/react-native-tracker",
+ "comment": "Add app install, foreground and background event and application entity tracking (#1396)",
+ "type": "none"
+ }
+ ],
+ "packageName": "@snowplow/react-native-tracker"
+}
\ No newline at end of file
diff --git a/trackers/react-native-tracker/src/constants.ts b/trackers/react-native-tracker/src/constants.ts
index a681fa37c..5c1fbb0d7 100644
--- a/trackers/react-native-tracker/src/constants.ts
+++ b/trackers/react-native-tracker/src/constants.ts
@@ -2,10 +2,14 @@ export const FOREGROUND_EVENT_SCHEMA = 'iglu:com.snowplowanalytics.snowplow/appl
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 APPLICATION_INSTALL_EVENT_SCHEMA = 'iglu:com.snowplowanalytics.mobile/application_install/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 LIFECYCLE_CONTEXT_SCHEMA = 'iglu:com.snowplowanalytics.mobile/application_lifecycle/jsonschema/1-0-0';
+export const MOBILE_APPLICATION_CONTEXT_SCHEMA = 'iglu:com.snowplowanalytics.mobile/application/jsonschema/1-0-0';
+export const APPLICATION_CONTEXT_SCHEMA = 'iglu:com.snowplowanalytics.snowplow/application/jsonschema/1-0-0';
export const PAGE_URL_PROPERTY = 'url';
export const PAGE_REFERRER_PROPERTY = 'refr';
diff --git a/trackers/react-native-tracker/src/plugins/app_context/index.ts b/trackers/react-native-tracker/src/plugins/app_context/index.ts
new file mode 100644
index 000000000..417020df2
--- /dev/null
+++ b/trackers/react-native-tracker/src/plugins/app_context/index.ts
@@ -0,0 +1,44 @@
+import { CorePluginConfiguration, SelfDescribingJson } from '@snowplow/tracker-core';
+import { AppLifecycleConfiguration } from '../../types';
+import { APPLICATION_CONTEXT_SCHEMA, MOBILE_APPLICATION_CONTEXT_SCHEMA } from '../../constants';
+
+/**
+ * Tracks the application context entity with information about the app version.
+ * If appBuild is provided, a mobile application context is tracked, otherwise the Web equivalent is tracked.
+ *
+ * Entity schema if `appBuild` property is set: `iglu:com.snowplowanalytics.mobile/application/jsonschema/1-0-0`
+ * Entity schema if `appBuild` property is not set: `iglu:com.snowplowanalytics.snowplow/application/jsonschema/1-0-0`
+ */
+export function newAppContextPlugin({ appVersion, appBuild }: AppLifecycleConfiguration): CorePluginConfiguration {
+ const contexts = () => {
+ let entities: SelfDescribingJson[] = [];
+
+ if (appVersion) {
+ // Add application context to all events
+ if (appBuild) {
+ entities.push({
+ schema: MOBILE_APPLICATION_CONTEXT_SCHEMA,
+ data: {
+ version: appVersion,
+ build: appBuild,
+ },
+ });
+ } else {
+ entities.push({
+ schema: APPLICATION_CONTEXT_SCHEMA,
+ data: {
+ version: appVersion,
+ },
+ });
+ }
+ }
+
+ return entities;
+ };
+
+ return {
+ plugin: {
+ contexts,
+ },
+ };
+}
diff --git a/trackers/react-native-tracker/src/plugins/app_install/index.ts b/trackers/react-native-tracker/src/plugins/app_install/index.ts
new file mode 100644
index 000000000..4e9c8ac58
--- /dev/null
+++ b/trackers/react-native-tracker/src/plugins/app_install/index.ts
@@ -0,0 +1,37 @@
+import { buildSelfDescribingEvent, CorePluginConfiguration, TrackerCore } from '@snowplow/tracker-core';
+import { AppLifecycleConfiguration, TrackerConfiguration } from '../../types';
+import { APPLICATION_INSTALL_EVENT_SCHEMA } from '../../constants';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+/**
+ * Tracks an application install event on the first run of the app.
+ * Stores the install event in AsyncStorage to prevent tracking on subsequent runs.
+ *
+ * Event schema: `iglu:com.snowplowanalytics.mobile/application_install/jsonschema/1-0-0`
+ */
+export function newAppInstallPlugin(
+ { namespace, installAutotracking = false }: TrackerConfiguration & AppLifecycleConfiguration,
+ core: TrackerCore
+): CorePluginConfiguration {
+ if (installAutotracking) {
+ // Track install event on first run
+ const key = `snowplow_${namespace}_install`;
+ setTimeout(async () => {
+ const installEvent = await AsyncStorage.getItem(key);
+ if (!installEvent) {
+ core.track(
+ buildSelfDescribingEvent({
+ event: {
+ schema: APPLICATION_INSTALL_EVENT_SCHEMA,
+ data: {},
+ },
+ })
+ );
+ await AsyncStorage.setItem(key, new Date().toISOString());
+ }
+ }, 0);
+ }
+ return {
+ plugin: {},
+ };
+}
diff --git a/trackers/react-native-tracker/src/plugins/app_lifecycle/index.ts b/trackers/react-native-tracker/src/plugins/app_lifecycle/index.ts
new file mode 100644
index 000000000..c7eaae200
--- /dev/null
+++ b/trackers/react-native-tracker/src/plugins/app_lifecycle/index.ts
@@ -0,0 +1,97 @@
+import {
+ buildSelfDescribingEvent,
+ CorePluginConfiguration,
+ SelfDescribingJson,
+ TrackerCore,
+} from '@snowplow/tracker-core';
+import { AppLifecycleConfiguration, EventContext } from '../../types';
+import { BACKGROUND_EVENT_SCHEMA, FOREGROUND_EVENT_SCHEMA, LIFECYCLE_CONTEXT_SCHEMA } from '../../constants';
+import { AppState } from 'react-native';
+
+export interface AppLifecyclePlugin extends CorePluginConfiguration {
+ getIsInBackground: () => boolean | undefined;
+ getBackgroundIndex: () => number | undefined;
+ getForegroundIndex: () => number | undefined;
+}
+
+/**
+ * Tracks foreground and background events automatically when the app state changes.
+ * Also adds a lifecycle context to all events with information about the app visibility.
+ */
+export async function newAppLifecyclePlugin(
+ { lifecycleAutotracking = true }: AppLifecycleConfiguration,
+ core: TrackerCore
+): Promise {
+ let isInForeground = AppState.currentState !== 'background';
+ let foregroundIndex = isInForeground ? 1 : 0;
+ let backgroundIndex = isInForeground ? 0 : 1;
+ let subscription: ReturnType | undefined;
+
+ if (lifecycleAutotracking) {
+ // Subscribe to app state changes and track foreground/background events
+ subscription = AppState.addEventListener('change', async (nextAppState) => {
+ if (nextAppState === 'active' && !isInForeground) {
+ trackForegroundEvent();
+ }
+ if (nextAppState === 'background' && isInForeground) {
+ trackBackgroundEvent();
+ }
+ });
+ }
+
+ const contexts = () => {
+ let entities: SelfDescribingJson[] = [];
+
+ if (lifecycleAutotracking) {
+ // Add lifecycle context to all events
+ entities.push({
+ schema: LIFECYCLE_CONTEXT_SCHEMA,
+ data: {
+ isVisible: isInForeground,
+ index: isInForeground ? foregroundIndex : backgroundIndex,
+ },
+ });
+ }
+
+ return entities;
+ };
+
+ const deactivatePlugin = () => {
+ if (subscription) {
+ subscription.remove();
+ subscription = undefined;
+ }
+ };
+
+ const trackForegroundEvent = (contexts?: EventContext[]) => {
+ if (!isInForeground) {
+ isInForeground = true;
+ foregroundIndex += 1;
+ }
+ core.track(
+ buildSelfDescribingEvent({ event: { schema: FOREGROUND_EVENT_SCHEMA, data: { foregroundIndex } } }),
+ contexts
+ );
+ };
+
+ const trackBackgroundEvent = (contexts?: EventContext[]) => {
+ if (isInForeground) {
+ isInForeground = false;
+ backgroundIndex += 1;
+ }
+ core.track(
+ buildSelfDescribingEvent({ event: { schema: BACKGROUND_EVENT_SCHEMA, data: { backgroundIndex } } }),
+ contexts
+ );
+ };
+
+ return {
+ getIsInBackground: () => (lifecycleAutotracking ? !isInForeground : undefined),
+ getBackgroundIndex: () => (lifecycleAutotracking ? backgroundIndex : undefined),
+ getForegroundIndex: () => (lifecycleAutotracking ? foregroundIndex : undefined),
+ plugin: {
+ contexts,
+ deactivatePlugin,
+ },
+ };
+}
diff --git a/trackers/react-native-tracker/src/tracker.ts b/trackers/react-native-tracker/src/tracker.ts
index ef6608f92..86d375e38 100644
--- a/trackers/react-native-tracker/src/tracker.ts
+++ b/trackers/react-native-tracker/src/tracker.ts
@@ -14,6 +14,7 @@ import {
import {
DeepLinkConfiguration,
+ AppLifecycleConfiguration,
EventContext,
EventStoreConfiguration,
ListItemViewProps,
@@ -29,6 +30,9 @@ import { newSessionPlugin } from './plugins/session';
import { newDeepLinksPlugin } from './plugins/deep_links';
import { newPlugins } from './plugins';
import { newPlatformContextPlugin } from './plugins/platform_context';
+import { newAppLifecyclePlugin } from './plugins/app_lifecycle';
+import { newAppInstallPlugin } from './plugins/app_install';
+import { newAppContextPlugin } from './plugins/app_context';
const initializedTrackers: Record = {};
@@ -45,7 +49,8 @@ export async function newTracker(
EventStoreConfiguration &
ScreenTrackingConfiguration &
PlatformContextConfiguration &
- DeepLinkConfiguration
+ DeepLinkConfiguration &
+ AppLifecycleConfiguration
): Promise {
const { namespace, appId, encodeBase64 = false } = configuration;
if (configuration.eventStore === undefined) {
@@ -83,6 +88,15 @@ export async function newTracker(
const platformContextPlugin = await newPlatformContextPlugin(configuration);
addPlugin(platformContextPlugin);
+ const lifecyclePlugin = await newAppLifecyclePlugin(configuration, core);
+ addPlugin(lifecyclePlugin);
+
+ const installPlugin = newAppInstallPlugin(configuration, core);
+ addPlugin(installPlugin);
+
+ const appContextPlugin = newAppContextPlugin(configuration);
+ addPlugin(appContextPlugin);
+
(configuration.plugins ?? []).forEach((plugin) => addPlugin({ plugin }));
const tracker: ReactNativeTracker = {
@@ -128,6 +142,9 @@ export async function newTracker(
[namespace]
),
trackDeepLinkReceivedEvent: deepLinksPlugin.trackDeepLinkReceivedEvent,
+ getIsInBackground: lifecyclePlugin.getIsInBackground,
+ getBackgroundIndex: lifecyclePlugin.getBackgroundIndex,
+ getForegroundIndex: lifecyclePlugin.getForegroundIndex,
};
initializedTrackers[namespace] = { tracker, core };
diff --git a/trackers/react-native-tracker/src/types.ts b/trackers/react-native-tracker/src/types.ts
index 6441399e5..e078d3047 100755
--- a/trackers/react-native-tracker/src/types.ts
+++ b/trackers/react-native-tracker/src/types.ts
@@ -42,6 +42,44 @@ export interface SessionConfiguration {
backgroundSessionTimeout?: number;
}
+/**
+ * Configuration for app lifecycle tracking
+ */
+export interface AppLifecycleConfiguration {
+ /**
+ * Whether to automatically track app lifecycle events (app foreground and background events).
+ * Also adds a lifecycle context entity to all events.
+ *
+ * Foreground event schema: `iglu:com.snowplowanalytics.snowplow/application_foreground/jsonschema/1-0-0`
+ * Background event schema: `iglu:com.snowplowanalytics.snowplow/application_background/jsonschema/1-0-0`
+ * Context entity schema: `iglu:com.snowplowanalytics.mobile/application_lifecycle/jsonschema/1-0-0`
+ *
+ * @defaultValue true
+ */
+ lifecycleAutotracking?: boolean;
+ /**
+ * Whether to automatically track app install event on first run.
+ *
+ * Schema: `iglu:com.snowplowanalytics.mobile/application_install/jsonschema/1-0-0`
+ *
+ * @defaultValue false
+ */
+ installAutotracking?: boolean;
+ /**
+ * Version number of the application e.g 1.1.0 (semver or git commit hash).
+ *
+ * Entity schema if `appBuild` property is set: `iglu:com.snowplowanalytics.mobile/application/jsonschema/1-0-0`
+ * Entity schema if `appBuild` property is not set: `iglu:com.snowplowanalytics.snowplow/application/jsonschema/1-0-0`
+ */
+ appVersion?: string;
+ /**
+ * Build name of the application e.g s9f2k2d or 1.1.0 beta
+ *
+ * Entity schema: `iglu:com.snowplowanalytics.mobile/application/jsonschema/1-0-0`
+ */
+ appBuild?: string;
+}
+
/**
* The configuration object for initialising the tracker
*/
@@ -813,29 +851,20 @@ export type ReactNativeTracker = {
*/
readonly getSessionState: () => Promise;
- // TODO:
- // /**
- // * Gets whether the app is currently in background state
- // *
- // * @returns {Promise}
- // */
- // readonly getIsInBackground: () => Promise;
-
- // TODO:
- // /**
- // * Gets the number of background transitions in the current session
- // *
- // * @returns {Promise}
- // */
- // readonly getBackgroundIndex: () => Promise;
-
- // TODO:
- // /**
- // * Gets the number of foreground transitions in the current session.
- // *
- // * @returns {Promise}
- // */
- // readonly getForegroundIndex: () => Promise;
+ /**
+ * Gets whether the app is currently in background state
+ */
+ readonly getIsInBackground: () => boolean | undefined;
+
+ /**
+ * Gets the number of background transitions in the current session
+ */
+ readonly getBackgroundIndex: () => number | undefined;
+
+ /**
+ * Gets the number of foreground transitions in the current session.
+ */
+ readonly getForegroundIndex: () => number | undefined;
/**
* Enables tracking the platform context with information about the device.
diff --git a/trackers/react-native-tracker/test/plugins/app_context.test.ts b/trackers/react-native-tracker/test/plugins/app_context.test.ts
new file mode 100644
index 000000000..449f0efdd
--- /dev/null
+++ b/trackers/react-native-tracker/test/plugins/app_context.test.ts
@@ -0,0 +1,76 @@
+import { APPLICATION_CONTEXT_SCHEMA, MOBILE_APPLICATION_CONTEXT_SCHEMA } from '../../src/constants';
+import { newAppContextPlugin } from '../../src/plugins/app_context';
+import { buildPageView, Payload, trackerCore } from '@snowplow/tracker-core';
+
+describe('Application context plugin', () => {
+ it('attaches mobile application context to events if both version and build passed', async () => {
+ const appContext = await newAppContextPlugin({
+ appBuild: '19',
+ appVersion: '1.0.1',
+ });
+
+ const payloads: Payload[] = [];
+ const tracker = trackerCore({
+ corePlugins: [appContext.plugin],
+ callback: (pb) => payloads.push(pb.build()),
+ base64: false,
+ });
+
+ tracker.track(buildPageView({ pageUrl: 'http://localhost' }));
+
+ expect(payloads.length).toBe(1);
+ expect(JSON.parse(payloads[0]?.co as string).data).toEqual([
+ {
+ schema: MOBILE_APPLICATION_CONTEXT_SCHEMA,
+ data: {
+ version: '1.0.1',
+ build: '19',
+ },
+ },
+ ]);
+ });
+
+ it('attaches application context to events if only version passed', async () => {
+ const appContext = await newAppContextPlugin({
+ appVersion: '1.0.1',
+ });
+
+ const payloads: Payload[] = [];
+ const tracker = trackerCore({
+ corePlugins: [appContext.plugin],
+ callback: (pb) => payloads.push(pb.build()),
+ base64: false,
+ });
+
+ tracker.track(buildPageView({ pageUrl: 'http://localhost' }));
+
+ expect(payloads.length).toBe(1);
+ expect(JSON.parse(payloads[0]?.co as string).data).toEqual([
+ {
+ schema: APPLICATION_CONTEXT_SCHEMA,
+ data: {
+ version: '1.0.1',
+ },
+ },
+ ]);
+ });
+
+ it('doesnt attach any application context to events if version not passed', async () => {
+ const appContext = await newAppContextPlugin({
+ appBuild: '19',
+ });
+
+ const payloads: Payload[] = [];
+ const tracker = trackerCore({
+ corePlugins: [appContext.plugin],
+ callback: (pb) => payloads.push(pb.build()),
+ base64: false,
+ });
+
+ tracker.track(buildPageView({ pageUrl: 'http://localhost' }));
+
+ expect(payloads.length).toBe(1);
+ expect((payloads[0]?.co as string) ?? '').not.toContain(MOBILE_APPLICATION_CONTEXT_SCHEMA);
+ expect((payloads[0]?.co as string) ?? '').not.toContain(APPLICATION_CONTEXT_SCHEMA);
+ });
+});
diff --git a/trackers/react-native-tracker/test/plugins/app_install.test.ts b/trackers/react-native-tracker/test/plugins/app_install.test.ts
new file mode 100644
index 000000000..88b9409f5
--- /dev/null
+++ b/trackers/react-native-tracker/test/plugins/app_install.test.ts
@@ -0,0 +1,90 @@
+import { Payload, trackerCore } from '@snowplow/tracker-core';
+import { newAppInstallPlugin } from '../../src/plugins/app_install';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { APPLICATION_INSTALL_EVENT_SCHEMA } from '../../src/constants';
+
+describe('Application install plugin', () => {
+ beforeEach(async () => {
+ await AsyncStorage.clear();
+ });
+
+ it('tracks an app install event on first tracker init', async () => {
+ const tracker = trackerCore({
+ callback: (pb) => payloads.push(pb.build()),
+ base64: false,
+ });
+ const appInstallPlugin = newAppInstallPlugin(
+ {
+ namespace: 'test',
+ installAutotracking: true,
+ },
+ tracker
+ );
+ tracker.addPlugin(appInstallPlugin);
+
+ const payloads: Payload[] = [];
+
+ await new Promise((resolve) => setTimeout(resolve, 50));
+
+ expect(payloads.length).toBe(1);
+ const [{ ue_pr }] = payloads as any;
+ expect(ue_pr).toContain(APPLICATION_INSTALL_EVENT_SCHEMA);
+ });
+
+ it('does not track an app install event on subsequent tracker inits', async () => {
+ const tracker1 = trackerCore({
+ callback: (pb) => payloads.push(pb.build()),
+ base64: false,
+ });
+ tracker1.addPlugin(
+ newAppInstallPlugin(
+ {
+ namespace: 'test',
+ installAutotracking: true,
+ },
+ tracker1
+ )
+ );
+
+ const payloads: Payload[] = [];
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ expect(payloads.length).toBe(1);
+
+ const tracker2 = trackerCore({
+ callback: (pb) => payloads.push(pb.build()),
+ base64: false,
+ });
+ tracker2.addPlugin(
+ newAppInstallPlugin(
+ {
+ namespace: 'test',
+ installAutotracking: true,
+ },
+ tracker2
+ )
+ );
+
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ expect(payloads.length).toBe(1);
+ });
+
+ it('does not track an app install event when autotracking is disabled', async () => {
+ const tracker = trackerCore({
+ callback: (pb) => payloads.push(pb.build()),
+ base64: false,
+ });
+ const appInstallPlugin = newAppInstallPlugin(
+ {
+ namespace: 'test',
+ },
+ tracker
+ );
+ tracker.addPlugin(appInstallPlugin);
+
+ const payloads: Payload[] = [];
+
+ await new Promise((resolve) => setTimeout(resolve, 50));
+
+ expect(payloads.length).toBe(0);
+ });
+});
diff --git a/trackers/react-native-tracker/test/plugins/app_lifecycle.test.ts b/trackers/react-native-tracker/test/plugins/app_lifecycle.test.ts
new file mode 100644
index 000000000..f4cb35824
--- /dev/null
+++ b/trackers/react-native-tracker/test/plugins/app_lifecycle.test.ts
@@ -0,0 +1,97 @@
+import { AppState } from 'react-native';
+import { BACKGROUND_EVENT_SCHEMA, LIFECYCLE_CONTEXT_SCHEMA } from '../../src/constants';
+import { buildPageView, Payload, trackerCore } from '@snowplow/tracker-core';
+import { newAppLifecyclePlugin } from '../../src/plugins/app_lifecycle';
+
+describe('Application lifecycle plugin', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('tracks events on app state changes', async () => {
+ const appStateSpy = jest.spyOn(AppState, 'addEventListener');
+
+ const payloads: Payload[] = [];
+ const tracker = trackerCore({
+ callback: (pb) => payloads.push(pb.build()),
+ base64: false,
+ });
+ const appLifecyclePlugin = await newAppLifecyclePlugin({}, tracker);
+ tracker.addPlugin(appLifecyclePlugin);
+
+ appStateSpy.mock.calls?.[0]?.[1]('background');
+
+ expect(payloads.length).toBe(1);
+ expect(payloads[0]?.ue_pr ?? '').toContain(BACKGROUND_EVENT_SCHEMA);
+ });
+
+ it('attaches lifecycle context to events with the correct properties', async () => {
+ const appStateSpy = jest.spyOn(AppState, 'addEventListener');
+
+ const payloads: Payload[] = [];
+ const tracker = trackerCore({
+ callback: (pb) => payloads.push(pb.build()),
+ base64: false,
+ });
+ const appLifecyclePlugin = await newAppLifecyclePlugin({}, tracker);
+ tracker.addPlugin(appLifecyclePlugin);
+
+ tracker.track(buildPageView({ pageUrl: 'http://localhost' }));
+
+ expect(payloads.length).toBe(1);
+ expect(JSON.parse(payloads[0]?.co as string).data).toEqual([
+ {
+ schema: LIFECYCLE_CONTEXT_SCHEMA,
+ data: {
+ isVisible: true,
+ index: 1,
+ },
+ },
+ ]);
+
+ payloads.length = 0;
+ appStateSpy.mock.calls?.[0]?.[1]('background');
+
+ expect(payloads.length).toBe(1);
+ expect(JSON.parse(payloads[0]?.co as string).data).toEqual([
+ {
+ schema: LIFECYCLE_CONTEXT_SCHEMA,
+ data: {
+ isVisible: false,
+ index: 1,
+ },
+ },
+ ]);
+
+ payloads.length = 0;
+ appStateSpy.mock.calls?.[0]?.[1]('active');
+
+ expect(payloads.length).toBe(1);
+ expect(JSON.parse(payloads[0]?.co as string).data).toEqual([
+ {
+ schema: LIFECYCLE_CONTEXT_SCHEMA,
+ data: {
+ isVisible: true,
+ index: 2,
+ },
+ },
+ ]);
+ });
+
+ it('removes subscription on tracker deactivation', async () => {
+ const appStateSpy = jest.spyOn(AppState, 'addEventListener');
+ const removeSpy = jest.fn();
+ appStateSpy.mockReturnValue({ remove: removeSpy });
+
+ const tracker = trackerCore({
+ callback: () => {},
+ base64: false,
+ });
+ const appLifecyclePlugin = await newAppLifecyclePlugin({}, tracker);
+ tracker.addPlugin(appLifecyclePlugin);
+
+ expect(appStateSpy).toHaveBeenCalledTimes(1);
+ tracker.deactivate();
+ expect(removeSpy).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/trackers/react-native-tracker/test/tracker.test.ts b/trackers/react-native-tracker/test/tracker.test.ts
index 4ea74c75d..6be0a33a3 100644
--- a/trackers/react-native-tracker/test/tracker.test.ts
+++ b/trackers/react-native-tracker/test/tracker.test.ts
@@ -93,6 +93,28 @@ describe('Tracker', () => {
expect(await tracker.getSessionUserId()).toBeDefined();
});
+ it('attaches application context to events', async () => {
+ const tracker = await newTracker({
+ namespace: 'test',
+ appId: 'my-app',
+ appVersion: '1.0.1',
+ endpoint: 'http://localhost:9090',
+ customFetch: mockFetch,
+ });
+ tracker.trackPageViewEvent({
+ pageUrl: 'http://localhost:9090',
+ pageTitle: 'Home',
+ });
+ await tracker.flush();
+ expect(requests.length).toBe(1);
+
+ const [request] = requests;
+ const payload = await request?.json();
+ expect(payload.data.length).toBe(1);
+ expect(payload.data[0].co).toContain('/application/');
+ expect(payload.data[0].co).toContain('1.0.1');
+ });
+
it('tracks screen engagement events', async () => {
const tracker = await newTracker({
namespace: 'test',