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 da09aff2a..d33967db3 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): Promise;
+export declare function newTracker(configuration: TrackerConfiguration & EmitterConfiguration & SessionConfiguration & SubjectConfiguration & EventStoreConfiguration & ScreenTrackingConfiguration): 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) | 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 | 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 6d8f6dd05..82b9fdcfa 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
@@ -10,7 +10,11 @@ The ReactNativeTracker type
```typescript
export declare type ReactNativeTracker = {
+ namespace: string;
readonly trackSelfDescribingEvent: = Record>(argmap: SelfDescribingJson, contexts?: EventContext[]) => void;
+ readonly trackScreenViewEvent: (argmap: ScreenViewProps, contexts?: EventContext[]) => void;
+ readonly trackScrollChangedEvent: (argmap: ScrollChangedProps, contexts?: EventContext[]) => void;
+ readonly trackListItemViewEvent: (argmap: ListItemViewProps, contexts?: EventContext[]) => void;
readonly trackStructuredEvent: (argmap: StructuredEvent, contexts?: EventContext[]) => void;
readonly trackPageViewEvent: (argmap: PageViewEvent, contexts?: EventContext[]) => void;
readonly trackTimingEvent: (argmap: TimingProps, contexts?: EventContext[]) => void;
@@ -18,7 +22,7 @@ export declare type ReactNativeTracker = {
addGlobalContexts(contexts: Array | Record): void;
clearGlobalContexts(): void;
removeGlobalContexts(contexts: Array): void;
- addPlugin(configuration: CorePluginConfiguration): void;
+ addPlugin(configuration: BrowserPluginConfiguration): void;
flush: () => Promise;
readonly setAppId: (appId: string) => void;
readonly setPlatform: (value: string) => void;
@@ -39,5 +43,5 @@ export declare type ReactNativeTracker = {
readonly getSessionState: () => Promise;
};
```
-References: [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)
+References: [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)
diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.trackerconfiguration.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.trackerconfiguration.md
index d4ccd1cce..0815e0bb7 100644
--- a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.trackerconfiguration.md
+++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.trackerconfiguration.md
@@ -19,4 +19,5 @@ export interface TrackerConfiguration
| [appId?](./react-native-tracker.trackerconfiguration.appid.md) | string | (Optional) The application ID |
| [encodeBase64?](./react-native-tracker.trackerconfiguration.encodebase64.md) | boolean | (Optional) Whether unstructured events and custom contexts should be base64 encoded. |
| [namespace](./react-native-tracker.trackerconfiguration.namespace.md) | string | The namespace of the tracker |
+| [plugins?](./react-native-tracker.trackerconfiguration.plugins.md) | BrowserPlugin\[\] | (Optional) Inject plugins which will be evaluated for each event |
diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.trackerconfiguration.plugins.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.trackerconfiguration.plugins.md
new file mode 100644
index 000000000..d627b5be3
--- /dev/null
+++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.trackerconfiguration.plugins.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [TrackerConfiguration](./react-native-tracker.trackerconfiguration.md) > [plugins](./react-native-tracker.trackerconfiguration.plugins.md)
+
+## TrackerConfiguration.plugins property
+
+Inject plugins which will be evaluated for each event
+
+Signature:
+
+```typescript
+plugins?: BrowserPlugin[];
+```
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 8f8131227..668fa35f1 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
@@ -4,6 +4,10 @@
```ts
+import { BrowserPlugin } from '@snowplow/browser-tracker-core';
+import { BrowserPluginConfiguration } from '@snowplow/browser-tracker-core';
+import { ScreenTrackingConfiguration } from '@snowplow/browser-plugin-screen-tracking';
+
// @public
export type ConditionalContextProvider = FilterProvider | RuleSetProvider;
@@ -251,7 +255,7 @@ export type MessageNotificationProps = {
};
// @public
-export function newTracker(configuration: TrackerConfiguration & EmitterConfiguration & SessionConfiguration & SubjectConfiguration & EventStoreConfiguration): Promise;
+export function newTracker(configuration: TrackerConfiguration & EmitterConfiguration & SessionConfiguration & SubjectConfiguration & EventStoreConfiguration & ScreenTrackingConfiguration): Promise;
// @public
export interface PageViewEvent {
@@ -277,7 +281,11 @@ export interface PayloadBuilder {
// @public
export type ReactNativeTracker = {
+ namespace: string;
readonly trackSelfDescribingEvent: = Record>(argmap: SelfDescribingJson, contexts?: EventContext[]) => void;
+ readonly trackScreenViewEvent: (argmap: ScreenViewProps, contexts?: EventContext[]) => void;
+ readonly trackScrollChangedEvent: (argmap: ScrollChangedProps, contexts?: EventContext[]) => void;
+ readonly trackListItemViewEvent: (argmap: ListItemViewProps, contexts?: EventContext[]) => void;
readonly trackStructuredEvent: (argmap: StructuredEvent, contexts?: EventContext[]) => void;
readonly trackPageViewEvent: (argmap: PageViewEvent, contexts?: EventContext[]) => void;
readonly trackTimingEvent: (argmap: TimingProps, contexts?: EventContext[]) => void;
@@ -285,7 +293,7 @@ export type ReactNativeTracker = {
addGlobalContexts(contexts: Array | Record): void;
clearGlobalContexts(): void;
removeGlobalContexts(contexts: Array): void;
- addPlugin(configuration: CorePluginConfiguration): void;
+ addPlugin(configuration: BrowserPluginConfiguration): void;
flush: () => Promise;
readonly setAppId: (appId: string) => void;
readonly setPlatform: (value: string) => void;
@@ -426,6 +434,7 @@ export interface TrackerConfiguration {
appId?: string;
encodeBase64?: boolean;
namespace: string;
+ plugins?: BrowserPlugin[];
}
// @public
diff --git a/common/changes/@snowplow/browser-plugin-screen-tracking/issue-screen-tracking_2024-11-26-13-58.json b/common/changes/@snowplow/browser-plugin-screen-tracking/issue-screen-tracking_2024-11-26-13-58.json
new file mode 100644
index 000000000..58cf5e944
--- /dev/null
+++ b/common/changes/@snowplow/browser-plugin-screen-tracking/issue-screen-tracking_2024-11-26-13-58.json
@@ -0,0 +1,10 @@
+{
+ "changes": [
+ {
+ "packageName": "@snowplow/browser-plugin-screen-tracking",
+ "comment": "Add screen tracking plugin and add support for browser plugins (#1394)",
+ "type": "none"
+ }
+ ],
+ "packageName": "@snowplow/browser-plugin-screen-tracking"
+}
\ No newline at end of file
diff --git a/common/changes/@snowplow/react-native-tracker/issue-screen-tracking_2024-11-26-13-58.json b/common/changes/@snowplow/react-native-tracker/issue-screen-tracking_2024-11-26-13-58.json
new file mode 100644
index 000000000..07b248c37
--- /dev/null
+++ b/common/changes/@snowplow/react-native-tracker/issue-screen-tracking_2024-11-26-13-58.json
@@ -0,0 +1,10 @@
+{
+ "changes": [
+ {
+ "packageName": "@snowplow/react-native-tracker",
+ "comment": "Add screen tracking plugin and add support for browser plugins (#1394)",
+ "type": "none"
+ }
+ ],
+ "packageName": "@snowplow/react-native-tracker"
+}
\ No newline at end of file
diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json
index 8157b3617..1445937ab 100644
--- a/common/config/rush/browser-approved-packages.json
+++ b/common/config/rush/browser-approved-packages.json
@@ -126,6 +126,10 @@
"name": "@snowplow/browser-plugin-privacy-sandbox",
"allowedCategories": [ "trackers" ]
},
+ {
+ "name": "@snowplow/browser-plugin-screen-tracking",
+ "allowedCategories": [ "trackers" ]
+ },
{
"name": "@snowplow/browser-plugin-site-tracking",
"allowedCategories": [ "trackers" ]
diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml
index fc5f51ccc..ffa132588 100644
--- a/common/config/rush/pnpm-lock.yaml
+++ b/common/config/rush/pnpm-lock.yaml
@@ -1546,6 +1546,88 @@ importers:
specifier: ~4.6.2
version: 4.6.4
+ ../../plugins/browser-plugin-screen-tracking:
+ dependencies:
+ '@snowplow/browser-tracker-core':
+ specifier: workspace:*
+ version: link:../../libraries/browser-tracker-core
+ '@snowplow/tracker-core':
+ specifier: workspace:*
+ version: link:../../libraries/tracker-core
+ tslib:
+ specifier: ^2.3.1
+ version: 2.7.0
+ uuid:
+ specifier: ^10.0.0
+ version: 10.0.0
+ devDependencies:
+ '@ampproject/rollup-plugin-closure-compiler':
+ specifier: ~0.27.0
+ version: 0.27.0(rollup@2.70.2)
+ '@rollup/plugin-commonjs':
+ specifier: ~21.0.2
+ version: 21.0.3(rollup@2.70.2)
+ '@rollup/plugin-node-resolve':
+ specifier: ~13.1.3
+ version: 13.1.3(rollup@2.70.2)
+ '@types/jest':
+ specifier: ~28.1.1
+ version: 28.1.8
+ '@types/jsdom':
+ specifier: ~16.2.14
+ version: 16.2.15
+ '@types/lodash':
+ specifier: ~4.14.180
+ version: 4.14.202
+ '@types/uuid':
+ specifier: ^10.0.0
+ version: 10.0.0
+ '@typescript-eslint/eslint-plugin':
+ specifier: ~5.15.0
+ version: 5.15.0(@typescript-eslint/parser@5.15.0(eslint@8.11.0)(typescript@4.6.4))(eslint@8.11.0)(typescript@4.6.4)
+ '@typescript-eslint/parser':
+ specifier: ~5.15.0
+ version: 5.15.0(eslint@8.11.0)(typescript@4.6.4)
+ eslint:
+ specifier: ~8.11.0
+ version: 8.11.0
+ jest:
+ specifier: ~28.1.3
+ version: 28.1.3(@types/node@20.16.3)(ts-node@10.9.2(@types/node@20.16.3)(typescript@4.6.4))
+ jest-environment-jsdom:
+ specifier: ~28.1.3
+ version: 28.1.3
+ jest-environment-jsdom-global:
+ specifier: ~4.0.0
+ version: 4.0.0(jest-environment-jsdom@28.1.3)
+ jest-standard-reporter:
+ specifier: ~2.0.0
+ version: 2.0.0
+ lodash:
+ specifier: ~4.17.21
+ version: 4.17.21
+ rollup:
+ specifier: ~2.70.1
+ version: 2.70.2
+ rollup-plugin-cleanup:
+ specifier: ~3.2.1
+ version: 3.2.1(rollup@2.70.2)
+ rollup-plugin-license:
+ specifier: ~2.6.1
+ version: 2.6.1(rollup@2.70.2)
+ rollup-plugin-terser:
+ specifier: ~7.0.2
+ version: 7.0.2(rollup@2.70.2)
+ rollup-plugin-ts:
+ specifier: ~2.0.5
+ version: 2.0.7(@babel/core@7.25.2)(@babel/plugin-transform-runtime@7.25.9(@babel/core@7.25.2))(@babel/preset-env@7.26.0(@babel/core@7.25.2))(@babel/runtime@7.25.6)(rollup@2.70.2)(typescript@4.6.4)
+ ts-jest:
+ specifier: ~28.0.8
+ version: 28.0.8(@babel/core@7.25.2)(@jest/types@28.1.3)(babel-jest@28.1.3(@babel/core@7.25.2))(jest@28.1.3(@types/node@20.16.3)(ts-node@10.9.2(@types/node@20.16.3)(typescript@4.6.4)))(typescript@4.6.4)
+ typescript:
+ specifier: ~4.6.2
+ version: 4.6.4
+
../../plugins/browser-plugin-site-tracking:
dependencies:
'@snowplow/browser-tracker-core':
@@ -2361,6 +2443,12 @@ importers:
'@react-native-async-storage/async-storage':
specifier: ~2.0.0
version: 2.0.0(react-native@0.74.5(@babel/preset-env@7.26.0(@babel/core@7.25.2))(@types/react@18.3.12)(encoding@0.1.13)(react@18.2.0))
+ '@snowplow/browser-plugin-screen-tracking':
+ specifier: workspace:*
+ version: link:../../plugins/browser-plugin-screen-tracking
+ '@snowplow/browser-tracker-core':
+ specifier: workspace:*
+ version: link:../../libraries/browser-tracker-core
'@snowplow/tracker-core':
specifier: workspace:*
version: link:../../libraries/tracker-core
@@ -2374,6 +2462,9 @@ importers:
specifier: ^10.0.0
version: 10.0.0
devDependencies:
+ '@snowplow/browser-plugin-snowplow-ecommerce':
+ specifier: workspace:*
+ version: link:../../plugins/browser-plugin-snowplow-ecommerce
'@types/jest':
specifier: ~28.1.1
version: 28.1.8
diff --git a/common/config/rush/repo-state.json b/common/config/rush/repo-state.json
index d50fdf0a1..df90b414c 100644
--- a/common/config/rush/repo-state.json
+++ b/common/config/rush/repo-state.json
@@ -1,5 +1,5 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
- "pnpmShrinkwrapHash": "50fa04b4f2a1f9fdbf83a84ce901759859832bfd",
+ "pnpmShrinkwrapHash": "d1933251824ef3eb837586f089856ce0f6036111",
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
}
diff --git a/plugins/browser-plugin-screen-tracking/CHANGELOG.json b/plugins/browser-plugin-screen-tracking/CHANGELOG.json
new file mode 100644
index 000000000..ae43026dc
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/CHANGELOG.json
@@ -0,0 +1,5 @@
+{
+ "name": "@snowplow/browser-plugin-screen-tracking",
+ "entries": [
+ ]
+}
diff --git a/plugins/browser-plugin-screen-tracking/CHANGELOG.md b/plugins/browser-plugin-screen-tracking/CHANGELOG.md
new file mode 100644
index 000000000..b16611867
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/CHANGELOG.md
@@ -0,0 +1,3 @@
+# Change Log - @snowplow/browser-plugin-snowplow-screen-tracking
+
+This log was last generated on Fri, 01 Nov 2024 10:35:07 GMT and should not be manually modified.
diff --git a/plugins/browser-plugin-screen-tracking/LICENSE b/plugins/browser-plugin-screen-tracking/LICENSE
new file mode 100644
index 000000000..76f1946ea
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2022 Snowplow Analytics Ltd, 2010 Anthon Pang
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/plugins/browser-plugin-screen-tracking/README.md b/plugins/browser-plugin-screen-tracking/README.md
new file mode 100644
index 000000000..f89baa099
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/README.md
@@ -0,0 +1,59 @@
+# Snowplow Screen Tracking Plugin
+
+[![npm version][npm-image]][npm-url]
+[![License][license-image]](LICENSE)
+
+Browser Plugin to be used with `@snowplow/browser-tracker`. The plugin is already included in `@snowplow/react-native-tracker`.
+
+This plugin is the recommended way to track screen view events.
+
+## Maintainer quick start
+
+Part of the Snowplow JavaScript Tracker monorepo.
+Build with [Node.js](https://nodejs.org/en/) (18 - 20) and [Rush](https://rushjs.io/).
+
+### Setup repository
+
+```bash
+npm install -g @microsoft/rush
+git clone https://github.com/snowplow/snowplow-javascript-tracker.git
+rush update
+```
+
+## Package Installation
+
+With npm:
+
+```bash
+npm install @snowplow/browser-plugin-snowplow-screen-tracking
+```
+
+## Usage
+
+Initialize your tracker with the ScreenTrackingPlugin:
+
+```js
+import { newTracker } from '@snowplow/browser-tracker';
+import { ScreenTrackingPlugin } from '@snowplow/browser-plugin-screen-tracking';
+
+newTracker('sp1', '{{collector_url}}', {
+ appId: 'my-app-id',
+ plugins: [ ScreenTrackingPlugin() ],
+});
+```
+
+For a full API reference, you can read the plugin [documentation page](https://docs.snowplow.io/docs/collecting-data/collecting-from-own-applications/javascript-trackers/browser-tracker/browser-tracker-v3-reference/plugins/screen-tracking/).
+
+## Copyright and license
+
+Licensed and distributed under the [BSD 3-Clause License](LICENSE) ([An OSI Approved License][osi]).
+
+Copyright (c) 2022 Snowplow Analytics Ltd.
+
+All rights reserved.
+
+[npm-url]: https://www.npmjs.com/package/@snowplow/browser-plugin-screen-tracking
+[npm-image]: https://img.shields.io/npm/v/@snowplow/browser-plugin-screen-tracking
+[docs]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-tracker/
+[osi]: https://opensource.org/licenses/BSD-3-Clause
+[license-image]: https://img.shields.io/npm/l/@snowplow/browser-plugin-screen-tracking
diff --git a/plugins/browser-plugin-screen-tracking/jest.config.js b/plugins/browser-plugin-screen-tracking/jest.config.js
new file mode 100644
index 000000000..87d15da9b
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/jest.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ preset: 'ts-jest',
+ reporters: ['jest-standard-reporter'],
+ setupFilesAfterEnv: ['../../setupTestGlobals.ts'],
+ testEnvironment: 'jest-environment-jsdom-global',
+};
diff --git a/plugins/browser-plugin-screen-tracking/package.json b/plugins/browser-plugin-screen-tracking/package.json
new file mode 100644
index 000000000..bf82d80f2
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/package.json
@@ -0,0 +1,57 @@
+{
+ "name": "@snowplow/browser-plugin-screen-tracking",
+ "version": "4.0.1",
+ "description": "Snowplow screen tracking",
+ "homepage": "http://bit.ly/sp-js",
+ "bugs": "https://github.com/snowplow/snowplow-javascript-tracker/issues",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/snowplow/snowplow-javascript-tracker.git"
+ },
+ "license": "BSD-3-Clause",
+ "author": "Peter Perlepes",
+ "sideEffects": false,
+ "main": "./dist/index.umd.js",
+ "module": "./dist/index.module.js",
+ "types": "./dist/index.module.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "rollup -c --silent --failAfterWarnings",
+ "test": "jest"
+ },
+ "dependencies": {
+ "@snowplow/browser-tracker-core": "workspace:*",
+ "@snowplow/tracker-core": "workspace:*",
+ "tslib": "^2.3.1",
+ "uuid": "^10.0.0"
+ },
+ "devDependencies": {
+ "@ampproject/rollup-plugin-closure-compiler": "~0.27.0",
+ "@rollup/plugin-commonjs": "~21.0.2",
+ "@rollup/plugin-node-resolve": "~13.1.3",
+ "@types/jest": "~28.1.1",
+ "@types/jsdom": "~16.2.14",
+ "@types/lodash": "~4.14.180",
+ "@types/uuid": "^10.0.0",
+ "@typescript-eslint/eslint-plugin": "~5.15.0",
+ "@typescript-eslint/parser": "~5.15.0",
+ "eslint": "~8.11.0",
+ "jest": "~28.1.3",
+ "jest-environment-jsdom": "~28.1.3",
+ "jest-environment-jsdom-global": "~4.0.0",
+ "jest-standard-reporter": "~2.0.0",
+ "lodash": "~4.17.21",
+ "rollup": "~2.70.1",
+ "rollup-plugin-cleanup": "~3.2.1",
+ "rollup-plugin-license": "~2.6.1",
+ "rollup-plugin-terser": "~7.0.2",
+ "rollup-plugin-ts": "~2.0.5",
+ "ts-jest": "~28.0.8",
+ "typescript": "~4.6.2"
+ },
+ "peerDependencies": {
+ "@snowplow/browser-tracker": "~4.0.1"
+ }
+}
diff --git a/plugins/browser-plugin-screen-tracking/rollup.config.js b/plugins/browser-plugin-screen-tracking/rollup.config.js
new file mode 100644
index 000000000..817d14255
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/rollup.config.js
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2022 Snowplow Analytics Ltd, 2010 Anthon Pang
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import { nodeResolve } from '@rollup/plugin-node-resolve';
+import commonjs from '@rollup/plugin-commonjs';
+import ts from 'rollup-plugin-ts'; // Prefered over @rollup/plugin-typescript as it bundles .d.ts files
+import { banner } from '../../banner';
+import compiler from '@ampproject/rollup-plugin-closure-compiler';
+import { terser } from 'rollup-plugin-terser';
+import cleanup from 'rollup-plugin-cleanup';
+import pkg from './package.json';
+import { builtinModules } from 'module';
+
+const umdPlugins = [nodeResolve({ browser: true }), commonjs(), ts()];
+const umdName = 'snowplowScreenTracking';
+
+export default [
+ // CommonJS (for Node) and ES module (for bundlers) build.
+ {
+ input: './src/index.ts',
+ plugins: [...umdPlugins, banner()],
+ treeshake: { moduleSideEffects: ['sha1'] },
+ output: [{ file: pkg.main, format: 'umd', sourcemap: true, name: umdName }],
+ },
+ {
+ input: './src/index.ts',
+ plugins: [...umdPlugins, compiler(), terser(), cleanup({ comments: 'none' }), banner()],
+ treeshake: { moduleSideEffects: ['sha1'] },
+ output: [{ file: pkg.main.replace('.js', '.min.js'), format: 'umd', sourcemap: true, name: umdName }],
+ },
+ {
+ input: './src/index.ts',
+ external: [...builtinModules, ...Object.keys(pkg.dependencies), ...Object.keys(pkg.devDependencies)],
+ plugins: [
+ ts(), // so Rollup can convert TypeScript to JavaScript
+ banner(),
+ ],
+ output: [{ file: pkg.module, format: 'es', sourcemap: true }],
+ },
+];
diff --git a/plugins/browser-plugin-screen-tracking/src/api.ts b/plugins/browser-plugin-screen-tracking/src/api.ts
new file mode 100644
index 000000000..761286e43
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/src/api.ts
@@ -0,0 +1,273 @@
+import { BrowserPlugin, BrowserTracker, dispatchToTrackersInCollection } from '@snowplow/browser-tracker-core';
+import { CommonEventProperties, Payload } from '@snowplow/tracker-core';
+import {
+ buildListItemViewEvent,
+ buildScreenEndEvent,
+ buildScreenEntity,
+ buildScreenSummaryEntity,
+ buildScreenViewEvent,
+ buildScrollChangedEvent,
+} from './core';
+import {
+ ListItemViewProps,
+ ScreenProps,
+ ScreenSummaryProps,
+ ScreenTrackingConfiguration,
+ ScreenViewProps,
+ ScrollChangedProps,
+} from './types';
+import {
+ BACKGROUND_EVENT_SCHEMA,
+ FOREGROUND_EVENT_SCHEMA,
+ LIST_ITEM_VIEW_EVENT_SCHEMA,
+ SCREEN_END_EVENT_SCHEMA,
+ SCREEN_VIEW_EVENT_SCHEMA,
+ SCROLL_CHANGED_EVENT_SCHEMA,
+} from './schemata';
+import { getUsefulSchemaAndData } from './utils';
+import { v4 as uuidv4 } from 'uuid';
+
+const _trackers: Record = {};
+
+/**
+ * Adds screen tracking
+ */
+export function ScreenTrackingPlugin({
+ screenEngagementAutotracking = true,
+ screenContext = true,
+}: ScreenTrackingConfiguration = {}): BrowserPlugin {
+ let trackerId: string;
+ let screenEntity: ScreenProps | undefined;
+ let screenSummary: ScreenSummaryProps | undefined;
+ let lastUpdate: Date | undefined;
+ let inForeground = true;
+
+ // Update the screen summary foreground and background durations based on the time since the last update
+ const updateScreenSummaryDurations = () => {
+ if (screenSummary !== undefined && lastUpdate !== undefined) {
+ const timeDiffSec = (new Date().getTime() - lastUpdate.getTime()) / 1000;
+ if (inForeground) {
+ screenSummary.foreground_sec += timeDiffSec;
+ } else {
+ screenSummary.background_sec = (screenSummary.background_sec ?? 0) + timeDiffSec;
+ }
+ lastUpdate = new Date();
+ }
+ };
+
+ // Update the screen summary scroll values based on the current event
+ const updateScreenSummaryScroll = (scrollChanged: ScrollChangedProps) => {
+ if (screenSummary) {
+ if (scrollChanged.yOffset !== undefined) {
+ const maxYOffset = scrollChanged.yOffset + (scrollChanged.viewHeight ?? 0);
+ screenSummary.max_y_offset = Math.max(maxYOffset, screenSummary.max_y_offset ?? maxYOffset);
+
+ screenSummary.min_y_offset = Math.min(
+ scrollChanged.yOffset,
+ screenSummary.min_y_offset ?? scrollChanged.yOffset
+ );
+ }
+
+ if (scrollChanged.xOffset !== undefined) {
+ const maxXOffset = scrollChanged.xOffset + (scrollChanged.viewWidth ?? 0);
+ screenSummary.max_x_offset = Math.max(maxXOffset, screenSummary.max_x_offset ?? maxXOffset);
+
+ screenSummary.min_x_offset = Math.min(
+ scrollChanged.xOffset,
+ screenSummary.min_x_offset ?? scrollChanged.xOffset
+ );
+ }
+
+ if (scrollChanged.contentHeight !== undefined) {
+ screenSummary.content_height = Math.max(scrollChanged.contentHeight, screenSummary.content_height ?? 0);
+ }
+
+ if (scrollChanged.contentWidth !== undefined) {
+ screenSummary.content_width = Math.max(scrollChanged.contentWidth, screenSummary.content_width ?? 0);
+ }
+ }
+ };
+
+ // Update the screen summary list items based on the current event
+ const updateScreenSummaryListItems = (listItemView: ListItemViewProps) => {
+ if (screenSummary) {
+ screenSummary.last_item_index = Math.max(
+ listItemView.index,
+ screenSummary?.last_item_index ?? listItemView.index
+ );
+ if (listItemView.itemsCount !== undefined) {
+ screenSummary.items_count = Math.max(
+ listItemView.itemsCount,
+ screenSummary.items_count ?? listItemView.itemsCount
+ );
+ }
+ listItemView.index;
+ }
+ };
+
+ // Update the current screen view state
+ const updateScreenView = (screenView: ScreenViewProps) => {
+ if (screenEntity && !screenView.previousId && !screenView.previousName) {
+ screenView.previousId = screenEntity.id;
+ screenView.previousName = screenEntity.name;
+ screenView.previousType = screenEntity.type;
+ }
+
+ const { name, id, type } = screenView;
+ if (name && id) {
+ screenEntity = { name, id, type };
+ screenSummary = { foreground_sec: 0 };
+ lastUpdate = new Date();
+ } else {
+ screenEntity = undefined;
+ screenSummary = undefined;
+ lastUpdate = undefined;
+ }
+ };
+
+ return {
+ activateBrowserPlugin: (tracker) => {
+ trackerId = tracker.id;
+ _trackers[trackerId] = tracker;
+ },
+
+ beforeTrack: (payload) => {
+ const schemaAndData = getUsefulSchemaAndData(payload);
+ if (schemaAndData) {
+ const { schema, data, eventPayload } = schemaAndData;
+
+ // For screen view events, we need to update the current state,
+ // and fill in missing previous references
+ if (schema === SCREEN_VIEW_EVENT_SCHEMA) {
+ const screenView = data as ScreenViewProps;
+ updateScreenView(screenView);
+
+ // Replace the event payload with the updated screen view
+ payload.addJson('ue_px', 'ue_pr', {
+ ...eventPayload,
+ data: { schema, data: screenView },
+ });
+ }
+
+ // For screen end events, we need to attach the screen summary entity
+ // These events are skipped if there is no screen summary to attach.
+ else if (schema === SCREEN_END_EVENT_SCHEMA) {
+ if (screenSummary && screenEngagementAutotracking) {
+ updateScreenSummaryDurations();
+ payload.addContextEntity(buildScreenSummaryEntity(screenSummary));
+ } else {
+ payload.add('__filter__', true);
+ }
+ }
+
+ // For foreground events, we need to attach the screen summary entity
+ else if (schema == FOREGROUND_EVENT_SCHEMA && screenEngagementAutotracking) {
+ updateScreenSummaryDurations();
+ inForeground = true;
+ if (screenSummary) {
+ payload.addContextEntity(buildScreenSummaryEntity(screenSummary));
+ }
+ }
+
+ // For background events, we need to attach the screen summary entity
+ else if (schema == BACKGROUND_EVENT_SCHEMA && screenEngagementAutotracking) {
+ updateScreenSummaryDurations();
+ inForeground = false;
+ if (screenSummary) {
+ payload.addContextEntity(buildScreenSummaryEntity(screenSummary));
+ }
+ }
+
+ // For list item view events, we need to update the current state for screen summary
+ // These events are skipped if screenEngagementAutotracking is enabled.
+ else if (schema == LIST_ITEM_VIEW_EVENT_SCHEMA && screenEngagementAutotracking) {
+ const listItemView = data as ListItemViewProps;
+ updateScreenSummaryListItems(listItemView);
+ payload.add('__filter__', true);
+ }
+
+ // For scroll changed events, we need to update the current state for screen summary
+ // These events are skipped if screenEngagementAutotracking is enabled.
+ else if (schema == SCROLL_CHANGED_EVENT_SCHEMA && screenEngagementAutotracking) {
+ const scrollChanged = data as ScrollChangedProps;
+ updateScreenSummaryScroll(scrollChanged);
+ payload.add('__filter__', true);
+ }
+ }
+
+ // For all events, we need to attach the screen entity if screenContext is enabled
+ if (screenEntity && screenContext) {
+ payload.addContextEntity(buildScreenEntity(screenEntity));
+ }
+ },
+
+ filter: (payload: Payload) => {
+ // Skip events that have been filtered out in the beforeTrack hook
+ return payload['__filter__'] === undefined;
+ },
+ };
+}
+
+/**
+ * Track a screen view event.
+ * If screen engagement tracking is enabled, will also track a `screen_end` event with the screen summary information of the previous screen view.
+ *
+ * Schema: `iglu:com.snowplowanalytics.mobile/screen_view/jsonschema/1-0-0`
+ *
+ * @param props - The properties of the screen view event
+ * @param trackers - The tracker identifiers which the event will be sent to
+ */
+export function trackScreenView(
+ props: ScreenViewProps & CommonEventProperties,
+ trackers: Array = Object.keys(_trackers)
+) {
+ const { context = [], timestamp, ...screenViewAttributes } = props;
+ if (!screenViewAttributes.id) {
+ screenViewAttributes.id = uuidv4();
+ }
+
+ dispatchToTrackersInCollection(trackers, _trackers, (t) => {
+ t.core.track(buildScreenEndEvent(), context, timestamp);
+ t.core.track(buildScreenViewEvent(screenViewAttributes), context, timestamp);
+ });
+}
+
+/**
+ * Event tracking the view of an item in a list.
+ * If screen engagement tracking is enabled, the list item view events will be aggregated into a `screen_summary` entity.
+ *
+ * Schema: `iglu:com.snowplowanalytics.mobile/list_item_view/jsonschema/1-0-0`
+ *
+ * @param props - The properties of the event
+ * @param trackers - The tracker identifiers which the event will be sent to
+ */
+export function trackListItemView(
+ props: ListItemViewProps & CommonEventProperties,
+ trackers: Array = Object.keys(_trackers)
+) {
+ const { context = [], timestamp, ...listItemViewAttributes } = props;
+
+ dispatchToTrackersInCollection(trackers, _trackers, (t) => {
+ t.core.track(buildListItemViewEvent(listItemViewAttributes), context, timestamp);
+ });
+}
+
+/**
+ * Event tracked when a scroll view's scroll position changes.
+ * If screen engagement tracking is enabled, the scroll changed events will be aggregated into a `screen_summary` entity.
+ *
+ * Schema: `iglu:com.snowplowanalytics.mobile/scroll_changed/jsonschema/1-0-0`
+ *
+ * @param props - The properties of the event
+ * @param trackers - The tracker identifiers which the event will be sent to
+ */
+export function trackScrollChanged(
+ props: ScrollChangedProps & CommonEventProperties,
+ trackers: Array = Object.keys(_trackers)
+) {
+ const { context = [], timestamp, ...scrollChangedAttributes } = props;
+
+ dispatchToTrackersInCollection(trackers, _trackers, (t) => {
+ t.core.track(buildScrollChangedEvent(scrollChangedAttributes), context, timestamp);
+ });
+}
diff --git a/plugins/browser-plugin-screen-tracking/src/core.ts b/plugins/browser-plugin-screen-tracking/src/core.ts
new file mode 100644
index 000000000..3cbba124d
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/src/core.ts
@@ -0,0 +1,70 @@
+import { buildSelfDescribingEvent } from '@snowplow/tracker-core';
+import { LIST_ITEM_VIEW_EVENT_SCHEMA, SCREEN_END_EVENT_SCHEMA, SCREEN_ENTITY_SCHEMA, SCREEN_SUMMARY_ENTITY_SCHEMA, SCREEN_VIEW_EVENT_SCHEMA, SCROLL_CHANGED_EVENT_SCHEMA } from './schemata';
+import { ListItemViewProps, ScreenProps, ScreenSummaryProps, ScreenViewProps, ScrollChangedProps } from './types';
+
+export function buildScreenViewEvent(event: ScreenViewProps) {
+ return buildSelfDescribingEvent({
+ event: {
+ schema: SCREEN_VIEW_EVENT_SCHEMA,
+ data: removeEmptyProperties({ ...event }),
+ },
+ });
+}
+
+export function buildScreenEndEvent() {
+ return buildSelfDescribingEvent({
+ event: {
+ schema: SCREEN_END_EVENT_SCHEMA,
+ data: {},
+ },
+ });
+}
+
+export function buildListItemViewEvent(event: ListItemViewProps) {
+ return buildSelfDescribingEvent({
+ event: {
+ schema: LIST_ITEM_VIEW_EVENT_SCHEMA,
+ data: removeEmptyProperties({ ...event }),
+ },
+ });
+}
+
+export function buildScrollChangedEvent(event: ScrollChangedProps) {
+ return buildSelfDescribingEvent({
+ event: {
+ schema: SCROLL_CHANGED_EVENT_SCHEMA,
+ data: removeEmptyProperties({ ...event }),
+ },
+ });
+}
+
+export function buildScreenEntity(entity: ScreenProps) {
+ return {
+ schema: SCREEN_ENTITY_SCHEMA,
+ data: removeEmptyProperties({ ...entity }),
+ };
+}
+
+export function buildScreenSummaryEntity(entity: ScreenSummaryProps) {
+ return {
+ schema: SCREEN_SUMMARY_ENTITY_SCHEMA,
+ data: removeEmptyProperties({ ...entity }),
+ };
+}
+
+/**
+ * Returns a copy of a JSON with undefined and null properties removed
+ *
+ * @param event - Object to clean
+ * @param exemptFields - Set of fields which should not be removed even if empty
+ * @returns A cleaned copy of eventJson
+ */
+function removeEmptyProperties(event: Record): Record {
+ const ret: Record = {};
+ for (const k in event) {
+ if (event[k] !== null && typeof event[k] !== 'undefined') {
+ ret[k] = event[k];
+ }
+ }
+ return ret;
+}
diff --git a/plugins/browser-plugin-screen-tracking/src/index.ts b/plugins/browser-plugin-screen-tracking/src/index.ts
new file mode 100644
index 000000000..4d4b4e299
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/src/index.ts
@@ -0,0 +1,2 @@
+export * from './api';
+export * from './types';
diff --git a/plugins/browser-plugin-screen-tracking/src/schemata.ts b/plugins/browser-plugin-screen-tracking/src/schemata.ts
new file mode 100644
index 000000000..d5710591e
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/src/schemata.ts
@@ -0,0 +1,9 @@
+export const SCREEN_VIEW_EVENT_SCHEMA = 'iglu:com.snowplowanalytics.mobile/screen_view/jsonschema/1-0-0';
+export const SCREEN_END_EVENT_SCHEMA = 'iglu:com.snowplowanalytics.mobile/screen_end/jsonschema/1-0-0';
+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 LIST_ITEM_VIEW_EVENT_SCHEMA = 'iglu:com.snowplowanalytics.mobile/list_item_view/jsonschema/1-0-0';
+export const SCROLL_CHANGED_EVENT_SCHEMA = 'iglu:com.snowplowanalytics.mobile/scroll_changed/jsonschema/1-0-0';
+
+export const SCREEN_ENTITY_SCHEMA = 'iglu:com.snowplowanalytics.mobile/screen/jsonschema/1-0-0';
+export const SCREEN_SUMMARY_ENTITY_SCHEMA = 'iglu:com.snowplowanalytics.mobile/screen_summary/jsonschema/1-0-0';
diff --git a/plugins/browser-plugin-screen-tracking/src/types.ts b/plugins/browser-plugin-screen-tracking/src/types.ts
new file mode 100644
index 000000000..22e3da4e1
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/src/types.ts
@@ -0,0 +1,141 @@
+export interface ScreenTrackingConfiguration {
+ /**
+ * Whether to enable tracking of the screen end event and the screen summary context entity.
+ * Make sure that you have lifecycle autotracking enabled for screen summary to have complete information.
+ * @default true
+ */
+ screenEngagementAutotracking?: boolean;
+
+ /**
+ * Whether to enable tracking of the screen context entity.
+ * @default true
+ */
+ screenContext?: boolean;
+}
+/**
+ * ScreenView event properties
+ * schema: iglu:com.snowplowanalytics.mobile/screen_view/jsonschema/1-0-0
+ */
+export type ScreenViewProps = {
+ /**
+ * The name of the screen viewed
+ */
+ name: string;
+ /**
+ * The id(UUID) of screen that was viewed
+ * Will be automatically generated if not provided
+ */
+ id?: string;
+ /**
+ * The type of screen that was viewed
+ */
+ type?: string;
+ /**
+ * The name of the previous screen that was viewed
+ */
+ previousName?: string;
+ /**
+ * The id(UUID) of the previous screen that was viewed
+ */
+ previousId?: string;
+ /**
+ * The type of the previous screen that was viewed
+ */
+ previousType?: string;
+ /**
+ * The type of transition that led to the screen being viewed
+ */
+ transitionType?: string;
+};
+
+/** Screen context entity properties. */
+export type ScreenProps = {
+ /** The name of the screen viewed. */
+ name: string;
+ /** The type of screen that was viewed e.g feed / carousel. */
+ type?: string;
+ /** An ID from the associated screenview event. */
+ id: string;
+ /** iOS specific: class name of the view controller. */
+ viewController?: string;
+ /** iOS specific: class name of the top level view controller. */
+ topViewController?: string;
+ /** Android specific: name of activity. */
+ activity?: string;
+ /** Android specific: name of fragment. */
+ fragment?: string;
+};
+
+/**
+ * Event tracked when a scroll view's scroll position changes.
+ * If screen engagement tracking is enabled, the scroll changed events will be aggregated into a `screen_summary` entity.
+ *
+ * Schema: `iglu:com.snowplowanalytics.mobile/scroll_changed/jsonschema/1-0-0`
+ */
+export type ScrollChangedProps = {
+ /**
+ * Vertical scroll offset in pixels
+ */
+ yOffset?: number;
+ /**
+ * Horizontal scroll offset in pixels.
+ */
+ xOffset?: number;
+ /**
+ * The height of the scroll view in pixels
+ */
+ viewHeight?: number;
+ /**
+ * The width of the scroll view in pixels
+ */
+ viewWidth?: number;
+ /**
+ * The height of the content in the scroll view in pixels
+ */
+ contentHeight?: number;
+ /**
+ * The width of the content in the scroll view in pixels
+ */
+ contentWidth?: number;
+};
+
+/**
+ * Event tracking the view of an item in a list.
+ * If screen engagement tracking is enabled, the list item view events will be aggregated into a `screen_summary` entity.
+ *
+ * Schema: `iglu:com.snowplowanalytics.mobile/list_item_view/jsonschema/1-0-0`
+ */
+export type ListItemViewProps = {
+ /**
+ * Index of the item in the list
+ */
+ index: number;
+ /**
+ * Total number of items in the list
+ */
+ itemsCount?: number;
+};
+
+/** Schema for an entity tracked with foreground/background/screen_end events with summary statistics about the screen view */
+export type ScreenSummaryProps = {
+ /** Time in seconds spent on the current screen while the app was in foreground */
+ foreground_sec: number;
+ /** Time in seconds spent on the current screen while the app was in background */
+ background_sec?: number;
+ /** Index of the last viewed item in the list on the screen */
+ last_item_index?: number;
+ /** Total number of items in the list on the screen */
+ items_count?: number;
+ /** Minimum horizontal scroll offset on the scroll view in pixels */
+ min_x_offset?: number;
+ /** Maximum horizontal scroll offset on the scroll view in pixels */
+ max_x_offset?: number;
+ /** Minimum vertical scroll offset on the scroll view in pixels */
+ min_y_offset?: number;
+ /** Maximum vertical scroll offset on the scroll view in pixels */
+ max_y_offset?: number;
+ /** Width of the scroll view in pixels */
+ content_width?: number;
+ /** Height of the scroll view in pixels */
+ content_height?: number;
+};
diff --git a/plugins/browser-plugin-screen-tracking/src/utils.ts b/plugins/browser-plugin-screen-tracking/src/utils.ts
new file mode 100644
index 000000000..faa1baac4
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/src/utils.ts
@@ -0,0 +1,18 @@
+import { PayloadBuilder } from '@snowplow/tracker-core';
+
+// Returns the "useful" schema, i.e. what would someone want to use to identify events.
+// For some events this is the 'e' property but for unstructured events, this is the
+// 'schema' from the 'ue_px' field.
+export function getUsefulSchemaAndData(sb: PayloadBuilder) {
+ let eventJson = sb.getJson();
+ for (const json of eventJson) {
+ if (json.keyIfEncoded === 'ue_px' && typeof json.json['data'] === 'object') {
+ const schema = (json.json['data'] as Record)['schema'];
+ if (typeof schema == 'string') {
+ const data = json.json['data'] as Record;
+ return { schema, data: data.data as Record, eventPayload: data };
+ }
+ }
+ }
+ return undefined;
+}
diff --git a/plugins/browser-plugin-screen-tracking/test/screen.test.ts b/plugins/browser-plugin-screen-tracking/test/screen.test.ts
new file mode 100644
index 000000000..bb440fd4e
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/test/screen.test.ts
@@ -0,0 +1,110 @@
+import { addTracker, SharedState, EventStore, BrowserTracker } from '@snowplow/browser-tracker-core';
+import { ScreenTrackingPlugin, trackScreenView } from '../src';
+import { newInMemoryEventStore } from '@snowplow/tracker-core';
+import { SCREEN_ENTITY_SCHEMA, SCREEN_VIEW_EVENT_SCHEMA } from '../src/schemata';
+
+describe('ScreenTrackingPlugin', () => {
+ let idx = 1;
+ let eventStore: EventStore;
+ let tracker: BrowserTracker | null;
+
+ describe('Enabled screen context tracking', () => {
+ beforeEach(() => {
+ eventStore = newInMemoryEventStore({});
+ const customFetch = async () => new Response(null, { status: 500 });
+ tracker = addTracker(`sp${idx++}`, `sp${idx++}`, 'js-3.0.0', '', new SharedState(), {
+ plugins: [ScreenTrackingPlugin()],
+ eventStore,
+ customFetch,
+ contexts: { webPage: false },
+ });
+ });
+
+ it('adds id and previous screen view references', async () => {
+ trackScreenView({
+ id: '1',
+ name: 'Home',
+ });
+
+ let [{ ue_pr }] = await eventStore.getAllPayloads();
+ let event = JSON.parse(ue_pr as string).data;
+ expect(event.schema).toBe(SCREEN_VIEW_EVENT_SCHEMA);
+ expect(event.data).toMatchObject({
+ name: 'Home',
+ id: '1',
+ });
+
+ trackScreenView({
+ name: 'About',
+ });
+
+ [, , { ue_pr }] = await eventStore.getAllPayloads();
+ event = JSON.parse(ue_pr as string).data;
+ expect(event.schema).toBe(SCREEN_VIEW_EVENT_SCHEMA);
+ expect(event.data).toMatchObject({
+ name: 'About',
+ id: expect.any(String),
+ previousName: 'Home',
+ previousId: '1',
+ });
+ });
+
+ it('adds screen context entity to all events', async () => {
+ trackScreenView({
+ id: '1',
+ name: 'Home',
+ });
+
+ let [{ co }] = await eventStore.getAllPayloads();
+ let context = JSON.parse(co as string).data;
+ expect(context).toEqual([
+ {
+ schema: SCREEN_ENTITY_SCHEMA,
+ data: {
+ name: 'Home',
+ id: '1',
+ },
+ },
+ ]);
+
+ tracker?.trackPageView();
+
+ [, { co }] = await eventStore.getAllPayloads();
+ context = JSON.parse(co as string).data;
+ expect(context).toEqual([
+ {
+ schema: SCREEN_ENTITY_SCHEMA,
+ data: {
+ name: 'Home',
+ id: '1',
+ },
+ },
+ ]);
+ });
+ });
+
+ describe('Disabled screen context tracking', () => {
+ beforeEach(() => {
+ eventStore = newInMemoryEventStore({});
+ const customFetch = async () => new Response(null, { status: 500 });
+ tracker = addTracker(`sp${idx++}`, `sp${idx++}`, 'js-3.0.0', '', new SharedState(), {
+ plugins: [ScreenTrackingPlugin({ screenContext: false })],
+ eventStore,
+ customFetch,
+ contexts: { webPage: false },
+ });
+ });
+
+ it('does not add screen context entity to events', async () => {
+ trackScreenView({ name: 'Home' });
+
+ let [{ co }] = await eventStore.getAllPayloads();
+ expect(co).toBeUndefined();
+
+ tracker?.trackPageView();
+
+ [, { co }] = await eventStore.getAllPayloads();
+ expect(co).toBeUndefined();
+ });
+ });
+});
diff --git a/plugins/browser-plugin-screen-tracking/test/screen_summary.test.ts b/plugins/browser-plugin-screen-tracking/test/screen_summary.test.ts
new file mode 100644
index 000000000..f86454980
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/test/screen_summary.test.ts
@@ -0,0 +1,215 @@
+import { addTracker, SharedState, EventStore, BrowserTracker } from '@snowplow/browser-tracker-core';
+import { ScreenTrackingPlugin, trackListItemView, trackScreenView, trackScrollChanged } from '../src';
+import { buildSelfDescribingEvent, newInMemoryEventStore } from '@snowplow/tracker-core';
+import { BACKGROUND_EVENT_SCHEMA, SCREEN_END_EVENT_SCHEMA, SCREEN_SUMMARY_ENTITY_SCHEMA, SCREEN_VIEW_EVENT_SCHEMA } from '../src/schemata';
+
+describe('Screen summary tracking', () => {
+ let idx = 1;
+ let eventStore: EventStore;
+ let tracker: BrowserTracker | null;
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.clearAllTimers();
+ });
+
+ describe('Enabled screen engagement tracking', () => {
+ beforeEach(() => {
+ eventStore = newInMemoryEventStore({});
+ const customFetch = async () => new Response(null, { status: 500 });
+ tracker = addTracker(`sp${idx++}`, `sp${idx++}`, 'js-3.0.0', '', new SharedState(), {
+ plugins: [ScreenTrackingPlugin()],
+ eventStore,
+ customFetch,
+ contexts: { webPage: false },
+ });
+ });
+
+ it('adds a screen summary entity to screen end event', async () => {
+ jest.setSystemTime(new Date('2022-04-17T00:00:00.000Z'));
+ trackScreenView({ name: 'Home' });
+
+ jest.setSystemTime(new Date('2022-04-17T00:00:15.000Z'));
+ trackScreenView({ name: 'About' });
+
+ const [, { ue_pr, co }] = await eventStore.getAllPayloads();
+ const event = JSON.parse(ue_pr as string).data;
+ expect(event.schema).toBe(SCREEN_END_EVENT_SCHEMA);
+
+ const context = JSON.parse(co as string).data;
+ const screenSummary = context.find((c: any) => c.schema === SCREEN_SUMMARY_ENTITY_SCHEMA);
+ expect(screenSummary.data).toMatchObject({
+ foreground_sec: 15,
+ });
+
+ jest.setSystemTime(new Date('2022-04-17T00:00:45.000Z'));
+ trackScreenView({ name: 'Contact' });
+
+ const [, , , { ue_pr: ue_pr2, co: co2 }] = await eventStore.getAllPayloads();
+ const event2 = JSON.parse(ue_pr2 as string).data;
+ expect(event2.schema).toBe(SCREEN_END_EVENT_SCHEMA);
+
+ const context2 = JSON.parse(co2 as string).data;
+ const screenSummary2 = context2.find((c: any) => c.schema === SCREEN_SUMMARY_ENTITY_SCHEMA);
+ expect(screenSummary2.data).toMatchObject({
+ foreground_sec: 30,
+ });
+ });
+
+ it('tracks both background and foreground time', async () => {
+ jest.setSystemTime(new Date('2022-04-17T00:00:00.000Z'));
+ trackScreenView({ name: 'Home' });
+
+ jest.setSystemTime(new Date('2022-04-17T00:00:15.000Z'));
+ tracker?.core.track(
+ buildSelfDescribingEvent({
+ event: {
+ schema: BACKGROUND_EVENT_SCHEMA,
+ data: {},
+ },
+ })
+ );
+
+ jest.setSystemTime(new Date('2022-04-17T00:00:25.000Z'));
+ trackScreenView({ name: 'About' });
+
+ const [, { ue_pr, co }, { ue_pr: ue_pr2, co: co2 }] = await eventStore.getAllPayloads();
+
+ const event = JSON.parse(ue_pr as string).data;
+ expect(event.schema).toBe(BACKGROUND_EVENT_SCHEMA);
+
+ const context = JSON.parse(co as string).data;
+ const screenSummary = context.find((c: any) => c.schema === SCREEN_SUMMARY_ENTITY_SCHEMA);
+ expect(screenSummary.data).toMatchObject({
+ foreground_sec: 15,
+ });
+
+ const event2 = JSON.parse(ue_pr2 as string).data;
+ expect(event2.schema).toBe(SCREEN_END_EVENT_SCHEMA);
+
+ const context2 = JSON.parse(co2 as string).data;
+ const screenSummary2 = context2.find((c: any) => c.schema === SCREEN_SUMMARY_ENTITY_SCHEMA);
+ expect(screenSummary2.data).toMatchObject({
+ foreground_sec: 15,
+ background_sec: 10,
+ });
+ });
+
+ it('adds scroll information to screen summary entity', async () => {
+ trackScreenView({ name: 'Home' });
+
+ trackScrollChanged({
+ yOffset: 10,
+ xOffset: 0,
+ viewHeight: 1000,
+ viewWidth: 100,
+ contentHeight: 2000,
+ contentWidth: 1000,
+ });
+
+ trackScrollChanged({
+ yOffset: 500,
+ xOffset: 10,
+ viewHeight: 1000,
+ viewWidth: 100,
+ contentHeight: 2000,
+ contentWidth: 1000,
+ });
+
+ trackScreenView({ name: 'About' });
+
+ const payloads = await eventStore.getAllPayloads();
+ expect(payloads.length).toBe(3);
+ const [, { co }] = payloads;
+ const context = JSON.parse(co as string).data;
+ const screenSummary = context.find((c: any) => c.schema === SCREEN_SUMMARY_ENTITY_SCHEMA);
+ expect(screenSummary.data).toMatchObject({
+ min_x_offset: 0,
+ max_x_offset: 10 + 100,
+ min_y_offset: 10,
+ max_y_offset: 500 + 1000,
+ content_height: 2000,
+ content_width: 1000,
+ });
+ });
+
+ it('adds list item view information to screen summary entity', async () => {
+ trackScreenView({ name: 'Home' });
+
+ trackListItemView({
+ index: 0,
+ itemsCount: 10,
+ });
+
+ trackListItemView({
+ index: 5,
+ itemsCount: 10,
+ });
+
+ trackScreenView({ name: 'About' });
+
+ const payloads = await eventStore.getAllPayloads();
+ expect(payloads.length).toBe(3);
+ const [, { co }] = payloads;
+ const context = JSON.parse(co as string).data;
+ const screenSummary = context.find((c: any) => c.schema === SCREEN_SUMMARY_ENTITY_SCHEMA);
+ expect(screenSummary.data).toMatchObject({
+ last_item_index: 5,
+ items_count: 10,
+ });
+ });
+ });
+
+ describe('Disabled screen engagement tracking', () => {
+ beforeEach(() => {
+ eventStore = newInMemoryEventStore({});
+ const customFetch = async () => new Response(null, { status: 500 });
+ tracker = addTracker(`sp${idx++}`, `sp${idx++}`, 'js-3.0.0', '', new SharedState(), {
+ plugins: [ScreenTrackingPlugin({ screenEngagementAutotracking: false })],
+ eventStore,
+ customFetch,
+ contexts: { webPage: false },
+ });
+ });
+
+ it('does not add a screen end event', async () => {
+ jest.setSystemTime(new Date('2022-04-17T00:00:00.000Z'));
+ trackScreenView({ name: 'Home' });
+
+ jest.setSystemTime(new Date('2022-04-17T00:00:15.000Z'));
+ trackScreenView({ name: 'About' });
+
+ const payloads = await eventStore.getAllPayloads();
+ expect(payloads.length).toBe(2);
+ const [, { ue_pr }] = payloads;
+ const event = JSON.parse(ue_pr as string).data;
+ expect(event.schema).toBe(SCREEN_VIEW_EVENT_SCHEMA);
+ });
+
+ it('tracks scroll and list item view events', async () => {
+ trackScreenView({ name: 'Home' });
+
+ trackScrollChanged({
+ yOffset: 10,
+ xOffset: 0,
+ viewHeight: 1000,
+ viewWidth: 100,
+ contentHeight: 2000,
+ contentWidth: 1000,
+ });
+
+ trackListItemView({
+ index: 0,
+ itemsCount: 10,
+ });
+
+ trackScreenView({ name: 'About' });
+
+ const payloads = await eventStore.getAllPayloads();
+ expect(payloads.length).toBe(4);
+ });
+ });
+});
diff --git a/plugins/browser-plugin-screen-tracking/tsconfig.json b/plugins/browser-plugin-screen-tracking/tsconfig.json
new file mode 100644
index 000000000..4082f16a5
--- /dev/null
+++ b/plugins/browser-plugin-screen-tracking/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../../tsconfig.json"
+}
diff --git a/rush.json b/rush.json
index d27d4df68..1251e9e73 100644
--- a/rush.json
+++ b/rush.json
@@ -540,6 +540,12 @@
"projectFolder": "plugins/browser-plugin-event-specifications",
"reviewCategory": "plugins",
"versionPolicyName": "tracker"
+ },
+ {
+ "packageName": "@snowplow/browser-plugin-screen-tracking",
+ "projectFolder": "plugins/browser-plugin-screen-tracking",
+ "reviewCategory": "plugins",
+ "versionPolicyName": "tracker"
}
]
}
diff --git a/trackers/react-native-tracker/package.json b/trackers/react-native-tracker/package.json
index 159a4b961..9e5fa46f4 100644
--- a/trackers/react-native-tracker/package.json
+++ b/trackers/react-native-tracker/package.json
@@ -48,12 +48,15 @@
},
"dependencies": {
"@snowplow/tracker-core": "workspace:*",
+ "@snowplow/browser-tracker-core": "workspace:*",
+ "@snowplow/browser-plugin-screen-tracking": "workspace:*",
"@react-native-async-storage/async-storage": "~2.0.0",
"react-native-get-random-values": "~1.11.0",
"tslib": "^2.3.1",
"uuid": "^10.0.0"
},
"devDependencies": {
+ "@snowplow/browser-plugin-snowplow-ecommerce": "workspace:*",
"@typescript-eslint/eslint-plugin": "~5.15.0",
"@typescript-eslint/parser": "~5.15.0",
"eslint": "~8.11.0",
diff --git a/trackers/react-native-tracker/src/plugins.ts b/trackers/react-native-tracker/src/plugins.ts
new file mode 100644
index 000000000..607ec3171
--- /dev/null
+++ b/trackers/react-native-tracker/src/plugins.ts
@@ -0,0 +1,71 @@
+import { BrowserPluginConfiguration, BrowserTracker, ParsedIdCookie } from '@snowplow/browser-tracker-core';
+import { TrackerCore } from '@snowplow/tracker-core';
+
+/**
+ * Creates a fake BrowserTracker from a TrackerCore instance in order to use in browser plugins.
+ * Most of the methods are not implemented and will throw an error if called.
+ * However, our plugins mostly only call the `core` methods.
+ */
+function toBrowserTracker(namespace: string, core: TrackerCore): BrowserTracker {
+ const notImplemented = () => {
+ throw new Error('Not implemented in React Native');
+ };
+ return {
+ id: namespace,
+ namespace,
+ core,
+ sharedState: {
+ bufferFlushers: [],
+ hasLoaded: true,
+ registeredOnLoadHandlers: [],
+ },
+ getDomainSessionIndex: () => 0,
+ getPageViewId: () => '',
+ getTabId: () => null,
+ getCookieName: () => '',
+ getUserId: () => undefined,
+ getDomainUserId: () => '',
+ getDomainUserInfo: (): ParsedIdCookie => ['', '', 0, 0, 0, undefined, '', '', '', undefined, 0],
+ setReferrerUrl: () => notImplemented,
+ setCustomUrl: () => notImplemented,
+ setDocumentTitle: () => notImplemented,
+ discardHashTag: () => notImplemented,
+ discardBrace: () => notImplemented,
+ setCookiePath: () => notImplemented,
+ setVisitorCookieTimeout: () => notImplemented,
+ newSession: () => notImplemented,
+ crossDomainLinker: () => notImplemented,
+ enableActivityTracking: () => notImplemented,
+ enableActivityTrackingCallback: () => notImplemented,
+ disableActivityTracking: () => notImplemented,
+ disableActivityTrackingCallback: () => notImplemented,
+ updatePageActivity: () => notImplemented,
+ setOptOutCookie: () => notImplemented,
+ setUserId: () => notImplemented,
+ setUserIdFromLocation: () => notImplemented,
+ setUserIdFromReferrer: () => notImplemented,
+ setUserIdFromCookie: () => notImplemented,
+ setCollectorUrl: () => notImplemented,
+ setBufferSize: () => notImplemented,
+ flushBuffer: () => notImplemented,
+ preservePageViewId: () => notImplemented,
+ preservePageViewIdForUrl: () => notImplemented,
+ trackPageView: () => notImplemented,
+ disableAnonymousTracking: () => notImplemented,
+ enableAnonymousTracking: () => notImplemented,
+ clearUserData: () => notImplemented,
+ addPlugin: () => notImplemented,
+ };
+}
+
+export function newPlugins(namespace: string, core: TrackerCore) {
+ return {
+ addPlugin: (plugin: BrowserPluginConfiguration) => {
+ core.addPlugin(plugin);
+ if (plugin.plugin.activateBrowserPlugin) {
+ const browserTracker = toBrowserTracker(namespace, core);
+ plugin.plugin.activateBrowserPlugin?.(browserTracker);
+ }
+ },
+ };
+}
diff --git a/trackers/react-native-tracker/src/tracker.ts b/trackers/react-native-tracker/src/tracker.ts
index ac5013d0e..94474339d 100644
--- a/trackers/react-native-tracker/src/tracker.ts
+++ b/trackers/react-native-tracker/src/tracker.ts
@@ -4,15 +4,21 @@ import { newEmitter } from '@snowplow/tracker-core';
import { newReactNativeEventStore } from './event_store';
import { newTrackEventFunctions } from './events';
import { newSubject } from './subject';
+import { ScreenTrackingConfiguration, ScreenTrackingPlugin, trackListItemView, trackScreenView, trackScrollChanged } from '@snowplow/browser-plugin-screen-tracking';
import {
+ EventContext,
EventStoreConfiguration,
+ ListItemViewProps,
ReactNativeTracker,
+ ScreenViewProps,
+ ScrollChangedProps,
SessionConfiguration,
SubjectConfiguration,
TrackerConfiguration,
} from './types';
import { newSessionPlugin } from './plugins/session';
+import { newPlugins } from './plugins';
const initializedTrackers: Record = {};
@@ -26,7 +32,8 @@ export async function newTracker(
EmitterConfiguration &
SessionConfiguration &
SubjectConfiguration &
- EventStoreConfiguration
+ EventStoreConfiguration &
+ ScreenTrackingConfiguration
): Promise {
const { namespace, appId, encodeBase64 = false } = configuration;
if (configuration.eventStore === undefined) {
@@ -37,13 +44,8 @@ export async function newTracker(
const callback = (payload: PayloadBuilder): void => {
emitter.input(payload.build());
};
- const core = trackerCore({ base64: encodeBase64, callback });
- const subject = newSubject(core, configuration);
- core.addPlugin(subject.subjectPlugin);
-
- const sessionPlugin = await newSessionPlugin(configuration);
- core.addPlugin(sessionPlugin);
+ const core = trackerCore({ base64: encodeBase64, callback });
core.setPlatform('mob'); // default platform
core.setTrackerVersion('rn-' + version);
core.setTrackerNamespace(namespace);
@@ -51,22 +53,61 @@ export async function newTracker(
core.setAppId(appId);
}
+ const { addPlugin } = newPlugins(namespace, core);
+
+ const sessionPlugin = await newSessionPlugin(configuration);
+ addPlugin(sessionPlugin);
+
+ const subject = newSubject(core, configuration);
+ addPlugin(subject.subjectPlugin);
+
+ const screenPlugin = ScreenTrackingPlugin(configuration);
+ addPlugin({ plugin: screenPlugin });
+
+ (configuration.plugins ?? []).forEach((plugin) => addPlugin({ plugin }));
+
const tracker: ReactNativeTracker = {
...newTrackEventFunctions(core),
...subject.properties,
+ namespace,
setAppId: core.setAppId,
setPlatform: core.setPlatform,
flush: emitter.flush,
addGlobalContexts: core.addGlobalContexts,
removeGlobalContexts: core.removeGlobalContexts,
clearGlobalContexts: core.clearGlobalContexts,
- addPlugin: core.addPlugin,
getSessionId: sessionPlugin.getSessionId,
getSessionIndex: sessionPlugin.getSessionIndex,
getSessionUserId: sessionPlugin.getSessionUserId,
getSessionState: sessionPlugin.getSessionState,
+ addPlugin,
+ trackScreenViewEvent: (argmap: ScreenViewProps, context?: EventContext[]) =>
+ trackScreenView(
+ {
+ ...argmap,
+ context,
+ },
+ [namespace]
+ ),
+ trackScrollChangedEvent: (argmap: ScrollChangedProps, context?: EventContext[]) =>
+ trackScrollChanged(
+ {
+ ...argmap,
+ context,
+ },
+ [namespace]
+ ),
+ trackListItemViewEvent: (argmap: ListItemViewProps, context?: EventContext[]) =>
+ trackListItemView(
+ {
+ ...argmap,
+ context,
+ },
+ [namespace]
+ ),
};
initializedTrackers[namespace] = { tracker, core };
+
return tracker;
}
diff --git a/trackers/react-native-tracker/src/types.ts b/trackers/react-native-tracker/src/types.ts
index a3f91361e..a0b7d94cf 100755
--- a/trackers/react-native-tracker/src/types.ts
+++ b/trackers/react-native-tracker/src/types.ts
@@ -1,7 +1,7 @@
+import { BrowserPlugin, BrowserPluginConfiguration } from '@snowplow/browser-tracker-core';
import {
ConditionalContextProvider,
ContextPrimitive,
- CorePluginConfiguration,
PageViewEvent,
SelfDescribingJson,
StructuredEvent,
@@ -55,6 +55,11 @@ export interface TrackerConfiguration {
* @defaultValue false
**/
encodeBase64?: boolean;
+ /**
+ * Inject plugins which will be evaluated for each event
+ * @defaultValue []
+ */
+ plugins?: BrowserPlugin[];
}
/**
@@ -375,6 +380,10 @@ export interface SessionState {
* The ReactNativeTracker type
*/
export type ReactNativeTracker = {
+ /**
+ * The namespace of the tracker
+ */
+ namespace: string;
/**
* Tracks a self-describing event
*
@@ -387,32 +396,29 @@ export type ReactNativeTracker = {
contexts?: EventContext[]
) => void;
- // TODO:
- // /**
- // * Tracks a screen-view event
- // *
- // * @param argmap - The screen-view event's properties
- // * @param contexts - The array of event contexts
- // */
- // readonly trackScreenViewEvent: (argmap: ScreenViewProps, contexts?: EventContext[]) => string | undefined;
+ /**
+ * Tracks a screen-view event
+ *
+ * @param argmap - The screen-view event's properties
+ * @param contexts - The array of event contexts
+ */
+ readonly trackScreenViewEvent: (argmap: ScreenViewProps, contexts?: EventContext[]) => void;
- // TODO:
- // /**
- // * Tracks a scroll changed event
- // *
- // * @param argmap - The scroll changed event's properties
- // * @param contexts - The array of event contexts
- // */
- // readonly trackScrollChangedEvent: (argmap: ScrollChangedProps, contexts?: EventContext[]) => string | undefined;
+ /**
+ * Tracks a scroll changed event
+ *
+ * @param argmap - The scroll changed event's properties
+ * @param contexts - The array of event contexts
+ */
+ readonly trackScrollChangedEvent: (argmap: ScrollChangedProps, contexts?: EventContext[]) => void;
- // TODO:
- // /**
- // * Tracks a list item view event
- // *
- // * @param argmap - The list item view event's properties
- // * @param contexts - The array of event contexts
- // */
- // readonly trackListItemViewEvent: (argmap: ListItemViewProps, contexts?: EventContext[]) => string | undefined;
+ /**
+ * Tracks a list item view event
+ *
+ * @param argmap - The list item view event's properties
+ * @param contexts - The array of event contexts
+ */
+ readonly trackListItemViewEvent: (argmap: ListItemViewProps, contexts?: EventContext[]) => void;
/**
* Tracks a structured event
@@ -480,7 +486,7 @@ export type ReactNativeTracker = {
* Add a plugin into the plugin collection after Core has already been initialised
* @param configuration - The plugin to add
*/
- addPlugin(configuration: CorePluginConfiguration): void;
+ addPlugin(configuration: BrowserPluginConfiguration): void;
/**
* Calls flush on all emitters in order to send all queued events to the collector
diff --git a/trackers/react-native-tracker/test/ecommerce.test.ts b/trackers/react-native-tracker/test/ecommerce.test.ts
new file mode 100644
index 000000000..7fbc06ee5
--- /dev/null
+++ b/trackers/react-native-tracker/test/ecommerce.test.ts
@@ -0,0 +1,54 @@
+import { newTracker } from '../src';
+import { setEcommerceUser, SnowplowEcommercePlugin, trackProductView } from '@snowplow/browser-plugin-snowplow-ecommerce';
+
+function createMockFetch(status: number, requests: Request[]) {
+ return async (input: Request) => {
+ requests.push(input);
+ let response = new Response(null, { status });
+ return response;
+ };
+}
+
+describe('Tracking ecommerce events using the ecomerce plugin', () => {
+ let requests: Request[];
+ let mockFetch: ReturnType;
+
+ beforeEach(async () => {
+ requests = [];
+ mockFetch = createMockFetch(200, requests);
+ });
+
+ it('tracks ecommerce events', async () => {
+ const tracker = await newTracker({
+ namespace: 'test',
+ endpoint: 'http://localhost:9090',
+ customFetch: mockFetch,
+ plugins: [SnowplowEcommercePlugin()],
+ });
+
+ setEcommerceUser({
+ id: 'my-user',
+ email: 'my-email@email.com',
+ });
+
+ trackProductView({
+ id: 'my-product',
+ name: 'My Product',
+ category: 'my-category',
+ price: 100,
+ currency: 'USD',
+ })
+
+ 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].ue_pr).toBeDefined();
+ expect(payload.data[0].ue_pr).toContain('/snowplow_ecommerce_action/');
+ expect(payload.data[0].co).toBeDefined();
+ expect(payload.data[0].co).toContain('My Product');
+ expect(payload.data[0].co).toContain('my-email@email.com');
+ });
+});
diff --git a/trackers/react-native-tracker/test/tracker.test.ts b/trackers/react-native-tracker/test/tracker.test.ts
index c1aef82b2..e66d1c580 100644
--- a/trackers/react-native-tracker/test/tracker.test.ts
+++ b/trackers/react-native-tracker/test/tracker.test.ts
@@ -92,6 +92,82 @@ describe('Tracker', () => {
expect(await tracker.getSessionUserId()).toBeDefined();
});
+ it('tracks screen engagement events', async () => {
+ const tracker = await newTracker({
+ namespace: 'test',
+ endpoint: 'http://localhost:9090',
+ customFetch: mockFetch,
+ });
+
+ tracker.trackScreenViewEvent({
+ name: '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].ue_pr).toBeDefined();
+ expect(payload.data[0].ue_pr).toContain('/screen_view/');
+ expect(payload.data[0].co).toContain('/screen/');
+
+ tracker.trackScrollChangedEvent({
+ xOffset: 101,
+ });
+ tracker.trackListItemViewEvent({
+ index: 1,
+ itemsCount: 909,
+ });
+ tracker.trackScreenViewEvent({ name: 'About' });
+ await tracker.flush();
+
+ expect(requests.length).toBe(2);
+ const [, secondRequest] = requests;
+ const secondPayload = await secondRequest?.json();
+ expect(secondPayload.data.length).toBe(2);
+ const [screenEndEvent] = secondPayload.data;
+ expect(screenEndEvent.ue_pr).toBeDefined();
+ expect(screenEndEvent.ue_pr).toContain('screen_end');
+ expect(screenEndEvent.co).toBeDefined();
+ expect(screenEndEvent.co).toContain('screen_summary');
+ expect(screenEndEvent.co).toContain('101');
+ expect(screenEndEvent.co).toContain('909');
+ });
+
+ it('doesnt track screen engagement events if disabled', async () => {
+ const tracker = await newTracker({
+ namespace: 'test',
+ endpoint: 'http://localhost:9090',
+ customFetch: mockFetch,
+ screenContext: false,
+ screenEngagementAutotracking: false,
+ });
+
+ tracker.trackScreenViewEvent({
+ name: 'Home',
+ });
+ tracker.trackScreenViewEvent({
+ name: 'About',
+ });
+ await tracker.flush();
+ expect(requests.length).toBe(1);
+
+ const [request] = requests;
+
+ const payload = await request?.json();
+ expect(payload.data.length).toBe(2);
+
+ const [screen1, screen2] = payload.data;
+ expect(screen1.ue_pr).toBeDefined();
+ expect(screen1.ue_pr).toContain('/screen_view/');
+ expect(screen1.co ?? '').not.toContain('/screen/');
+
+ expect(screen2.ue_pr).toBeDefined();
+ expect(screen2.ue_pr).toContain('/screen_view/');
+ expect(screen2.co ?? '').not.toContain('/screen/');
+ });
+
it('adds a tracker plugin', async () => {
const tracker = await newTracker({
namespace: 'test',