diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.getalltrackers.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.getalltrackers.md new file mode 100644 index 000000000..dd12938e2 --- /dev/null +++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.getalltrackers.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [getAllTrackers](./react-native-tracker.getalltrackers.md) + +## getAllTrackers() function + +Retrieves all initialized trackers + +Signature: + +```typescript +export declare function getAllTrackers(): ReactNativeTracker[]; +``` +Returns: + +[ReactNativeTracker](./react-native-tracker.reactnativetracker.md)\[\] + +All initialized trackers + diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.getwebviewcallback.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.getwebviewcallback.md new file mode 100644 index 000000000..e5313e453 --- /dev/null +++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.getwebviewcallback.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [getWebViewCallback](./react-native-tracker.getwebviewcallback.md) + +## getWebViewCallback() function + +Enables tracking events from apps rendered in react-native-webview components. The apps need to use the Snowplow WebView tracker to track the events. + +To subscribe for the events, set the `onMessage` attribute: <WebView onMessage={getWebViewCallback()} ... /> + +Signature: + +```typescript +export declare function getWebViewCallback(): (message: { + nativeEvent: { + data: string; + }; +}) => void; +``` +Returns: + +(message: { nativeEvent: { data: string; }; }) => void + +Callback to subscribe for events from Web views tracked using the Snowplow WebView tracker. + 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..c7d351cd6 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 @@ -15,7 +15,9 @@ | Function | Description | | --- | --- | +| [getAllTrackers()](./react-native-tracker.getalltrackers.md) | Retrieves all initialized trackers | | [getTracker(trackerNamespace)](./react-native-tracker.gettracker.md) | Retrieves an initialized tracker given its namespace | +| [getWebViewCallback()](./react-native-tracker.getwebviewcallback.md) | Enables tracking events from apps rendered in react-native-webview components. The apps need to use the Snowplow WebView tracker to track the events.To subscribe for the events, set the onMessage attribute: <WebView onMessage={getWebViewCallback()} ... /> | | [newTracker(configuration)](./react-native-tracker.newtracker.md) | Creates a new tracker instance with the given configuration | | [removeAllTrackers()](./react-native-tracker.removealltrackers.md) | Removes all initialized trackers | | [removeTracker(trackerNamespace)](./react-native-tracker.removetracker.md) | Removes a tracker given its namespace | @@ -46,7 +48,6 @@ | [RuleSet](./react-native-tracker.ruleset.md) | A ruleset has accept or reject properties that contain rules for matching Iglu schema URIs | | [SessionConfiguration](./react-native-tracker.sessionconfiguration.md) | Configuration for session tracking | | [SessionState](./react-native-tracker.sessionstate.md) | Current session state that is tracked in events. | -| [StructuredEvent](./react-native-tracker.structuredevent.md) | A Structured Event A classic style of event tracking, allows for easier movement between analytics systems. A loosely typed event, creating a Self Describing event is preferred, but useful for interoperability. | | [SubjectConfiguration](./react-native-tracker.subjectconfiguration.md) | Configuration of subject properties tracked with events | | [TrackerConfiguration](./react-native-tracker.trackerconfiguration.md) | The configuration object for initialising the tracker | | [TrackerCore](./react-native-tracker.trackercore.md) | Export interface containing all Core functions | @@ -84,7 +85,8 @@ | [ScreenSize](./react-native-tracker.screensize.md) | Screen size in pixels | | [ScreenViewProps](./react-native-tracker.screenviewprops.md) | ScreenView event properties schema: iglu:com.snowplowanalytics.mobile/screen\_view/jsonschema/1-0-0 | | [ScrollChangedProps](./react-native-tracker.scrollchangedprops.md) | 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 | -| [SelfDescribingJson](./react-native-tracker.selfdescribingjson.md) | Export interface for any Self-Describing JSON such as context or Self Describing events | +| [SelfDescribing](./react-native-tracker.selfdescribing.md) | Interface for any self-describing JSON such as context entities or self-describing events | +| [StructuredProps](./react-native-tracker.structuredprops.md) | A Structured Event A classic style of event tracking, allows for easier movement between analytics systems. A loosely typed event, creating a Self Describing event is preferred, but useful for interoperability. | | [Timestamp](./react-native-tracker.timestamp.md) | Algebraic datatype representing possible timestamp type choice | | [TimingProps](./react-native-tracker.timingprops.md) | Timing event properties | | [Trigger](./react-native-tracker.trigger.md) | Trigger for MessageNotification event | 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..a25a41512 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 @@ -11,11 +11,11 @@ The ReactNativeTracker type ```typescript export declare type ReactNativeTracker = { namespace: string; - readonly trackSelfDescribingEvent: = Record>(argmap: SelfDescribingJson, contexts?: EventContext[]) => void; + readonly trackSelfDescribingEvent: = Record>(argmap: SelfDescribing, 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 trackStructuredEvent: (argmap: StructuredProps, contexts?: EventContext[]) => void; readonly trackPageViewEvent: (argmap: PageViewEvent, contexts?: EventContext[]) => void; readonly trackTimingEvent: (argmap: TimingProps, contexts?: EventContext[]) => void; readonly trackDeepLinkReceivedEvent: (argmap: DeepLinkReceivedProps, contexts?: EventContext[]) => void; @@ -47,5 +47,5 @@ export declare type ReactNativeTracker = { readonly refreshPlatformContext: () => Promise; }; ``` -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), [DeepLinkReceivedProps](./react-native-tracker.deeplinkreceivedprops.md), [MessageNotificationProps](./react-native-tracker.messagenotificationprops.md), [ScreenSize](./react-native-tracker.screensize.md), [SubjectConfiguration](./react-native-tracker.subjectconfiguration.md), [SessionState](./react-native-tracker.sessionstate.md) +References: [SelfDescribing](./react-native-tracker.selfdescribing.md), [EventContext](./react-native-tracker.eventcontext.md), [ScreenViewProps](./react-native-tracker.screenviewprops.md), [ScrollChangedProps](./react-native-tracker.scrollchangedprops.md), [ListItemViewProps](./react-native-tracker.listitemviewprops.md), [StructuredProps](./react-native-tracker.structuredprops.md), [TimingProps](./react-native-tracker.timingprops.md), [DeepLinkReceivedProps](./react-native-tracker.deeplinkreceivedprops.md), [MessageNotificationProps](./react-native-tracker.messagenotificationprops.md), [ScreenSize](./react-native-tracker.screensize.md), [SubjectConfiguration](./react-native-tracker.subjectconfiguration.md), [SessionState](./react-native-tracker.sessionstate.md) diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.selfdescribing.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.selfdescribing.md new file mode 100644 index 000000000..1efbed825 --- /dev/null +++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.selfdescribing.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [SelfDescribing](./react-native-tracker.selfdescribing.md) + +## SelfDescribing type + +Interface for any self-describing JSON such as context entities or self-describing events + +Signature: + +```typescript +export declare type SelfDescribing> = SelfDescribingJson; +``` diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.selfdescribingjson.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.selfdescribingjson.md deleted file mode 100644 index 0a32034ac..000000000 --- a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.selfdescribingjson.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [SelfDescribingJson](./react-native-tracker.selfdescribingjson.md) - -## SelfDescribingJson type - -Export interface for any Self-Describing JSON such as context or Self Describing events - -Signature: - -```typescript -type SelfDescribingJson> = { - schema: string; - data: T extends any[] ? never : T extends {} ? T : never; -}; -``` diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.action.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.action.md deleted file mode 100644 index a1e9ec061..000000000 --- a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.action.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [StructuredEvent](./react-native-tracker.structuredevent.md) > [action](./react-native-tracker.structuredevent.action.md) - -## StructuredEvent.action property - -Signature: - -```typescript -action: string; -``` diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.category.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.category.md deleted file mode 100644 index 0e1a2cbb6..000000000 --- a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.category.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [StructuredEvent](./react-native-tracker.structuredevent.md) > [category](./react-native-tracker.structuredevent.category.md) - -## StructuredEvent.category property - -Signature: - -```typescript -category: string; -``` diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.label.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.label.md deleted file mode 100644 index 2d8731a8d..000000000 --- a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.label.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [StructuredEvent](./react-native-tracker.structuredevent.md) > [label](./react-native-tracker.structuredevent.label.md) - -## StructuredEvent.label property - -Signature: - -```typescript -label?: string; -``` diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.md deleted file mode 100644 index 0ce7bdde8..000000000 --- a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [StructuredEvent](./react-native-tracker.structuredevent.md) - -## StructuredEvent interface - -A Structured Event A classic style of event tracking, allows for easier movement between analytics systems. A loosely typed event, creating a Self Describing event is preferred, but useful for interoperability. - -Signature: - -```typescript -interface StructuredEvent -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [action](./react-native-tracker.structuredevent.action.md) | string | | -| [category](./react-native-tracker.structuredevent.category.md) | string | | -| [label?](./react-native-tracker.structuredevent.label.md) | string | (Optional) | -| [property?](./react-native-tracker.structuredevent.property.md) | string | (Optional) | -| [value?](./react-native-tracker.structuredevent.value.md) | number | (Optional) | - diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.property.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.property.md deleted file mode 100644 index 1015c9a8e..000000000 --- a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.property.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [StructuredEvent](./react-native-tracker.structuredevent.md) > [property](./react-native-tracker.structuredevent.property.md) - -## StructuredEvent.property property - -Signature: - -```typescript -property?: string; -``` diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.value.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.value.md deleted file mode 100644 index e7673c4ca..000000000 --- a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredevent.value.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [StructuredEvent](./react-native-tracker.structuredevent.md) > [value](./react-native-tracker.structuredevent.value.md) - -## StructuredEvent.value property - -Signature: - -```typescript -value?: number; -``` diff --git a/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredprops.md b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredprops.md new file mode 100644 index 000000000..2c4fda4c3 --- /dev/null +++ b/api-docs/docs/react-native-tracker/markdown/react-native-tracker.structuredprops.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@snowplow/react-native-tracker](./react-native-tracker.md) > [StructuredProps](./react-native-tracker.structuredprops.md) + +## StructuredProps type + +A Structured Event A classic style of event tracking, allows for easier movement between analytics systems. A loosely typed event, creating a Self Describing event is preferred, but useful for interoperability. + +Signature: + +```typescript +export declare type StructuredProps = StructuredEvent; +``` 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..82c214e95 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 @@ -16,6 +16,8 @@ export type ConditionalContextProvider = FilterProvider | RuleSetProvider; // @public export type ContextFilter = (args?: ContextEvent) => boolean; +// Warning: (ae-forgotten-export) The symbol "SelfDescribingJson" needs to be exported by the entry point index.d.ts +// // @public export type ContextGenerator = (args?: ContextEvent) => SelfDescribingJson | SelfDescribingJson[] | undefined; @@ -189,9 +191,19 @@ export interface FormFocusOrChangeEvent { value: string | null; } +// @public +export function getAllTrackers(): ReactNativeTracker[]; + // @public export function getTracker(trackerNamespace: string): ReactNativeTracker | undefined; +// @public +export function getWebViewCallback(): (message: { + nativeEvent: { + data: string; + }; +}) => void; + // @public export type JsonProcessor = (payloadBuilder: PayloadBuilder, jsonForProcessing: EventJson, contextEntitiesForProcessing: SelfDescribingJson[]) => void; @@ -346,11 +358,11 @@ export interface PlatformContextRetriever { // @public export type ReactNativeTracker = { namespace: string; - readonly trackSelfDescribingEvent: = Record>(argmap: SelfDescribingJson, contexts?: EventContext[]) => void; + readonly trackSelfDescribingEvent: = Record>(argmap: SelfDescribing, 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 trackStructuredEvent: (argmap: StructuredProps, contexts?: EventContext[]) => void; readonly trackPageViewEvent: (argmap: PageViewEvent, contexts?: EventContext[]) => void; readonly trackTimingEvent: (argmap: TimingProps, contexts?: EventContext[]) => void; readonly trackDeepLinkReceivedEvent: (argmap: DeepLinkReceivedProps, contexts?: EventContext[]) => void; @@ -435,10 +447,7 @@ export type ScrollChangedProps = { }; // @public -export type SelfDescribingJson> = { - schema: string; - data: T extends any[] ? never : T extends {} ? T : never; -}; +export type SelfDescribing> = SelfDescribingJson; // @public export interface SessionConfiguration { @@ -458,19 +467,10 @@ export interface SessionState { userId: string; } +// Warning: (ae-forgotten-export) The symbol "StructuredEvent" needs to be exported by the entry point index.d.ts +// // @public -export interface StructuredEvent { - // (undocumented) - action: string; - // (undocumented) - category: string; - // (undocumented) - label?: string; - // (undocumented) - property?: string; - // (undocumented) - value?: number; -} +export type StructuredProps = StructuredEvent; // @public export interface SubjectConfiguration { diff --git a/trackers/react-native-tracker/src/events.ts b/trackers/react-native-tracker/src/events.ts index 1195b83f2..02e1ef523 100644 --- a/trackers/react-native-tracker/src/events.ts +++ b/trackers/react-native-tracker/src/events.ts @@ -4,10 +4,9 @@ import { buildStructEvent, PageViewEvent, SelfDescribingJson, - StructuredEvent, TrackerCore, } from '@snowplow/tracker-core'; -import { EventContext, MessageNotificationProps, TimingProps } from './types'; +import { EventContext, MessageNotificationProps, StructuredProps, TimingProps } from './types'; export function newTrackEventFunctions(core: TrackerCore) { const trackSelfDescribingEvent = = Record>( @@ -17,7 +16,7 @@ export function newTrackEventFunctions(core: TrackerCore) { core.track(buildSelfDescribingEvent({ event: argmap }), contexts); }; - const trackStructuredEvent = (argmap: StructuredEvent, contexts?: EventContext[]) => { + const trackStructuredEvent = (argmap: StructuredProps, contexts?: EventContext[]) => { return core.track(buildStructEvent(argmap), contexts)?.eid; }; diff --git a/trackers/react-native-tracker/src/index.ts b/trackers/react-native-tracker/src/index.ts index 6c29689cb..a4cdb5590 100644 --- a/trackers/react-native-tracker/src/index.ts +++ b/trackers/react-native-tracker/src/index.ts @@ -2,4 +2,5 @@ import 'react-native-get-random-values'; export * from './types'; -export * from './tracker'; +export { newTracker, getTracker, getAllTrackers, removeTracker, removeAllTrackers } from './tracker'; +export * from './web_view_interface'; diff --git a/trackers/react-native-tracker/src/tracker.ts b/trackers/react-native-tracker/src/tracker.ts index ef6608f92..6eafe7ae9 100644 --- a/trackers/react-native-tracker/src/tracker.ts +++ b/trackers/react-native-tracker/src/tracker.ts @@ -143,6 +143,31 @@ export function getTracker(trackerNamespace: string): ReactNativeTracker | undef return initializedTrackers[trackerNamespace]?.tracker; } +/** + * Retrieves all initialized trackers + * @returns All initialized trackers + */ +export function getAllTrackers(): ReactNativeTracker[] { + return Object.values(initializedTrackers).map(({ tracker }) => tracker); +} + +/** + * Internal function to retrieve the tracker core given its namespace + * @param trackerNamespace - Tracker namespace + * @returns Tracker core if exists + */ +export function getTrackerCore(trackerNamespace: string): TrackerCore | undefined { + return initializedTrackers[trackerNamespace]?.core; +} + +/** + * Internal function to retrieve all initialized tracker cores + * @returns All initialized tracker cores + */ +export function getAllTrackerCores(): TrackerCore[] { + return Object.values(initializedTrackers).map(({ core }) => core); +} + /** * Removes a tracker given its namespace * diff --git a/trackers/react-native-tracker/src/types.ts b/trackers/react-native-tracker/src/types.ts index 6441399e5..e5e72b5f2 100755 --- a/trackers/react-native-tracker/src/types.ts +++ b/trackers/react-native-tracker/src/types.ts @@ -584,6 +584,20 @@ export interface SessionState { firstEventTimestamp?: string; } +/** + * A Structured Event + * A classic style of event tracking, allows for easier movement between analytics + * systems. A loosely typed event, creating a Self Describing event is preferred, but + * useful for interoperability. + */ +export type StructuredProps = StructuredEvent; + +/** + * Interface for any self-describing JSON such as context entities or self-describing events + * @typeParam T - The type of the data object within a SelfDescribingJson + */ +export type SelfDescribing> = SelfDescribingJson; + /** * The ReactNativeTracker type */ @@ -600,7 +614,7 @@ export type ReactNativeTracker = { * @typeParam TData - The type of the data object within the SelfDescribing object */ readonly trackSelfDescribingEvent: = Record>( - argmap: SelfDescribingJson, + argmap: SelfDescribing, contexts?: EventContext[] ) => void; @@ -634,7 +648,7 @@ export type ReactNativeTracker = { * @param argmap - The structured event properties * @param contexts - The array of event contexts */ - readonly trackStructuredEvent: (argmap: StructuredEvent, contexts?: EventContext[]) => void; + readonly trackStructuredEvent: (argmap: StructuredProps, contexts?: EventContext[]) => void; /** * Tracks a page-view event @@ -857,9 +871,7 @@ export type ReactNativeTracker = { export { version, PageViewEvent, - StructuredEvent, FormFocusOrChangeEvent, - SelfDescribingJson, Timestamp, PayloadBuilder, Payload, diff --git a/trackers/react-native-tracker/src/web_view_interface.ts b/trackers/react-native-tracker/src/web_view_interface.ts new file mode 100644 index 000000000..ec69dec29 --- /dev/null +++ b/trackers/react-native-tracker/src/web_view_interface.ts @@ -0,0 +1,215 @@ +'use strict'; + +import { buildSelfDescribingEvent, payloadBuilder } from '@snowplow/tracker-core'; +import { getAllTrackerCores, getAllTrackers, getTracker, getTrackerCore } from './tracker'; +import type { + ScreenViewProps, + SelfDescribing, + StructuredProps, + ReactNativeTracker, + TrackerCore, + PayloadBuilder, + Payload, +} from './types'; + +/** + * Internal event type for events with payload properties tracked using the WebView tracker. + */ +interface WebViewEvent { + selfDescribingEventData?: SelfDescribing; + eventName?: string; + trackerVersion?: string; + useragent?: string; + pageUrl?: string; + pageTitle?: string; + referrer?: string; + category?: string; + action?: string; + label?: string; + property?: string; + value?: number; + pingXOffsetMin?: number; + pingXOffsetMax?: number; + pingYOffsetMin?: number; + pingYOffsetMax?: number; +} + +/** + * Internal event type for page views tracked using the WebView tracker. + */ +interface WebViewPageViewEvent { + title?: string | null; + url?: string; + referrer?: string; +} + +/** + * Internal type exchanged in messages received from the WebView tracker in Web views through the web view callback. + */ +type WebViewMessage = { + command: 'trackSelfDescribingEvent' | 'trackStructEvent' | 'trackPageView' | 'trackScreenView' | 'trackWebViewEvent'; + event: StructuredProps | SelfDescribing | ScreenViewProps | WebViewPageViewEvent | WebViewEvent; + context?: Array | null; + trackers?: Array; +}; + +function forEachTracker(trackers: Array | undefined, iterator: (tracker: ReactNativeTracker) => void): void { + if (trackers && trackers.length > 0) { + trackers + .map(getTracker) + .filter((t) => t !== undefined) + .map((t) => t!) + .forEach(iterator); + } else { + getAllTrackers().forEach(iterator); + } +} + +function forEachTrackerCore(trackers: Array | undefined, iterator: (tracker: TrackerCore) => void): void { + if (trackers && trackers.length > 0) { + trackers + .map(getTrackerCore) + .filter((t) => t !== undefined) + .map((t) => t!) + .forEach(iterator); + } else { + getAllTrackerCores().forEach(iterator); + } +} + +/** + * Wrapper around the PayloadBuilder that disables overriding property values. + * This is to prevent the tracker overriding values like tracker version set in the WebView. + */ +function webViewPayloadBuilder(pb: PayloadBuilder): PayloadBuilder { + const addedKeys = new Set(); + + const add = (key: string, value: unknown): void => { + if (!addedKeys.has(key)) { + addedKeys.add(key); + pb.add(key, value); + } + }; + + const addDict = (dict: Payload): void => { + for (const key in dict) { + if (Object.prototype.hasOwnProperty.call(dict, key)) { + add(key, dict[key]); + } + } + }; + + return { + ...pb, + add, + addDict, + }; +} + +/** + * Enables tracking events from apps rendered in react-native-webview components. + * The apps need to use the Snowplow WebView tracker to track the events. + * + * To subscribe for the events, set the `onMessage` attribute: + * + * + * @returns Callback to subscribe for events from Web views tracked using the Snowplow WebView tracker. + */ +export function getWebViewCallback() { + return (message: { nativeEvent: { data: string } }): void => { + const data = JSON.parse(message.nativeEvent.data) as WebViewMessage; + switch (data.command) { + case 'trackSelfDescribingEvent': + forEachTracker(data.trackers, (tracker) => { + tracker.trackSelfDescribingEvent(data.event as SelfDescribing, data.context || []); + }); + break; + + case 'trackStructEvent': + forEachTracker(data.trackers, (tracker) => { + tracker.trackStructuredEvent(data.event as StructuredProps, data.context || []); + }); + break; + + case 'trackPageView': + forEachTracker(data.trackers, (tracker) => { + const event = data.event as WebViewPageViewEvent; + tracker.trackPageViewEvent({ + pageTitle: event.title ?? '', + pageUrl: event.url ?? '', + referrer: event.referrer, + }); + }); + break; + + case 'trackScreenView': + forEachTracker(data.trackers, (tracker) => { + tracker.trackScreenViewEvent(data.event as ScreenViewProps, data.context || []); + }); + break; + + case 'trackWebViewEvent': + forEachTrackerCore(data.trackers, (tracker) => { + const event = data.event as WebViewEvent; + let pb: PayloadBuilder; + if (event.selfDescribingEventData) { + pb = buildSelfDescribingEvent({ event: event.selfDescribingEventData }); + } else { + pb = payloadBuilder(); + } + pb = webViewPayloadBuilder(pb); + + if (event.eventName !== undefined) { + pb.add('e', event.eventName); + } + if (event.action !== undefined) { + pb.add('se_ac', event.action); + } + if (event.category !== undefined) { + pb.add('se_ca', event.category); + } + if (event.label !== undefined) { + pb.add('se_la', event.label); + } + if (event.property !== undefined) { + pb.add('se_pr', event.property); + } + if (event.value !== undefined) { + pb.add('se_va', event.value.toString()); + } + if (event.pageUrl !== undefined) { + pb.add('url', event.pageUrl); + } + if (event.pageTitle !== undefined) { + pb.add('page', event.pageTitle); + } + if (event.referrer !== undefined) { + pb.add('refr', event.referrer); + } + if (event.pingXOffsetMin !== undefined) { + pb.add('pp_mix', event.pingXOffsetMin.toString()); + } + if (event.pingXOffsetMax !== undefined) { + pb.add('pp_max', event.pingXOffsetMax.toString()); + } + if (event.pingYOffsetMin !== undefined) { + pb.add('pp_miy', event.pingYOffsetMin.toString()); + } + if (event.pingYOffsetMax !== undefined) { + pb.add('pp_may', event.pingYOffsetMax.toString()); + } + if (event.trackerVersion !== undefined) { + pb.add('tv', event.trackerVersion); + } + if (event.useragent !== undefined) { + pb.add('ua', event.useragent); + } + tracker.track(pb, data.context || []); + }); + break; + + default: + console.warn(`Unknown command from WebView: ${data.command}`); + } + }; +} diff --git a/trackers/react-native-tracker/test/web_view_interface.test.ts b/trackers/react-native-tracker/test/web_view_interface.test.ts new file mode 100644 index 000000000..da33775af --- /dev/null +++ b/trackers/react-native-tracker/test/web_view_interface.test.ts @@ -0,0 +1,386 @@ +import { getWebViewCallback, newTracker, removeTracker } from '../src'; + +function createMockFetch(status: number, requests: Request[]) { + return async (input: Request) => { + requests.push(input); + let response = new Response(null, { status }); + return response; + }; +} + +describe('WebView interface', () => { + let requests: Request[]; + let mockFetch: ReturnType; + + beforeEach(async () => { + requests = []; + mockFetch = createMockFetch(200, requests); + }); + + afterEach(() => { + removeTracker('test'); + }); + + it('tracks a page view event', async () => { + const tracker = await newTracker({ + namespace: 'test', + appId: 'my-app', + endpoint: 'http://localhost:9090', + customFetch: mockFetch, + }); + + const webViewInterface = getWebViewCallback(); + webViewInterface({ + nativeEvent: { + data: JSON.stringify({ + command: 'trackPageView', + event: { + title: 'Home', + url: 'http://localhost:9090', + referrer: 'http://refr.com', + }, + }), + }, + }); + + await tracker.flush(); + expect(requests.length).toBe(1); + + const [request] = requests; + const payload = await request?.json(); + expect(payload.data.length).toBe(1); + + const [event] = payload.data; + expect(event.e).toBe('pv'); + expect(event.url).toBe('http://localhost:9090'); + expect(event.refr).toBe('http://refr.com'); + expect(event.page).toBe('Home'); + }); + + it('tracks a self-describing event', async () => { + const tracker = await newTracker({ + namespace: 'test', + appId: 'my-app', + endpoint: 'http://localhost:9090', + customFetch: mockFetch, + encodeBase64: false, + }); + + const webViewInterface = getWebViewCallback(); + webViewInterface({ + nativeEvent: { + data: JSON.stringify({ + command: 'trackSelfDescribingEvent', + event: { + schema: 'iglu:com.snowplowanalytics.snowplow/event/jsonschema/1-0-0', + data: { + key: 'value', + }, + }, + }), + }, + }); + + await tracker.flush(); + expect(requests.length).toBe(1); + + const [request] = requests; + const payload = await request?.json(); + expect(payload.data.length).toBe(1); + + const [event] = payload.data; + const { e, ue_pr } = event; + expect(e).toBe('ue'); + expect(ue_pr).toBeDefined(); + const { data } = JSON.parse(ue_pr); + expect(data.schema).toBe('iglu:com.snowplowanalytics.snowplow/event/jsonschema/1-0-0'); + expect(data.data.key).toBe('value'); + }); + + it('tracks a structured event', async () => { + const tracker = await newTracker({ + namespace: 'test', + appId: 'my-app', + endpoint: 'http://localhost:9090', + customFetch: mockFetch, + }); + + const webViewInterface = getWebViewCallback(); + webViewInterface({ + nativeEvent: { + data: JSON.stringify({ + command: 'trackStructEvent', + event: { + category: 'category', + action: 'action', + label: 'label', + property: 'property', + value: 1, + }, + }), + }, + }); + + await tracker.flush(); + expect(requests.length).toBe(1); + + const [request] = requests; + const payload = await request?.json(); + expect(payload.data.length).toBe(1); + + const [event] = payload.data; + const { e, se_ca, se_ac, se_la, se_pr, se_va } = event; + expect(e).toBe('se'); + expect(se_ca).toBe('category'); + expect(se_ac).toBe('action'); + expect(se_la).toBe('label'); + expect(se_pr).toBe('property'); + expect(se_va).toBe('1'); + }); + + it('tracks a screen view event', async () => { + const tracker = await newTracker({ + namespace: 'test', + appId: 'my-app', + endpoint: 'http://localhost:9090', + customFetch: mockFetch, + encodeBase64: false, + }); + + const webViewInterface = getWebViewCallback(); + webViewInterface({ + nativeEvent: { + data: JSON.stringify({ + command: 'trackScreenView', + event: { + name: 'Home', + id: 'home', + }, + }), + }, + }); + + await tracker.flush(); + expect(requests.length).toBe(1); + + const [request] = requests; + const payload = await request?.json(); + expect(payload.data.length).toBe(1); + + const [event] = payload.data; + expect(event.e).toBe('ue'); + expect(event.ue_pr).toBeDefined(); + const { data } = JSON.parse(event.ue_pr); + expect(data.schema).toBe('iglu:com.snowplowanalytics.mobile/screen_view/jsonschema/1-0-0'); + expect(data.data.name).toBe('Home'); + expect(data.data.id).toBe('home'); + }); + + describe('WebView event tracking', () => { + + it('tracks a page view event', async () => { + const tracker = await newTracker({ + namespace: 'test', + appId: 'my-app', + endpoint: 'http://localhost:9090', + customFetch: mockFetch, + }); + + const webViewInterface = getWebViewCallback(); + webViewInterface({ + nativeEvent: { + data: JSON.stringify({ + command: 'trackWebViewEvent', + event: { + eventName: 'pv', + pageTitle: 'Home', + pageUrl: 'http://localhost:9090', + referrer: 'http://refr.com', + }, + }), + }, + }); + + await tracker.flush(); + expect(requests.length).toBe(1); + + const [request] = requests; + const payload = await request?.json(); + expect(payload.data.length).toBe(1); + + const [event] = payload.data; + expect(event.e).toBe('pv'); + expect(event.url).toBe('http://localhost:9090'); + expect(event.refr).toBe('http://refr.com'); + expect(event.page).toBe('Home'); + }); + + it('tracks a self-describing event', async () => { + const tracker = await newTracker({ + namespace: 'test', + appId: 'my-app', + endpoint: 'http://localhost:9090', + customFetch: mockFetch, + encodeBase64: false, + }); + + const webViewInterface = getWebViewCallback(); + webViewInterface({ + nativeEvent: { + data: JSON.stringify({ + command: 'trackWebViewEvent', + event: { + selfDescribingEventData: { + schema: 'iglu:com.snowplowanalytics.snowplow/event/jsonschema/1-0-0', + data: { + key: 'value', + }, + }, + }, + }), + }, + }); + + await tracker.flush(); + expect(requests.length).toBe(1); + + const [request] = requests; + const payload = await request?.json(); + expect(payload.data.length).toBe(1); + + const [event] = payload.data; + const { e, ue_pr } = event; + expect(e).toBe('ue'); + expect(ue_pr).toBeDefined(); + const { data } = JSON.parse(ue_pr); + expect(data.schema).toBe('iglu:com.snowplowanalytics.snowplow/event/jsonschema/1-0-0'); + expect(data.data.key).toBe('value'); + }); + + it('tracks a structured event', async () => { + const tracker = await newTracker({ + namespace: 'test', + appId: 'my-app', + endpoint: 'http://localhost:9090', + customFetch: mockFetch, + }); + + const webViewInterface = getWebViewCallback(); + webViewInterface({ + nativeEvent: { + data: JSON.stringify({ + command: 'trackWebViewEvent', + event: { + eventName: 'se', + category: 'category', + action: 'action', + label: 'label', + property: 'property', + value: 1, + }, + }), + }, + }); + + await tracker.flush(); + expect(requests.length).toBe(1); + + const [request] = requests; + const payload = await request?.json(); + expect(payload.data.length).toBe(1); + + const [event] = payload.data; + const { e, se_ca, se_ac, se_la, se_pr, se_va } = event; + expect(e).toBe('se'); + expect(se_ca).toBe('category'); + expect(se_ac).toBe('action'); + expect(se_la).toBe('label'); + expect(se_pr).toBe('property'); + expect(se_va).toBe('1'); + }); + + it('tracks a page ping event', async () => { + const tracker = await newTracker({ + namespace: 'test', + appId: 'my-app', + endpoint: 'http://localhost:9090', + customFetch: mockFetch, + }); + + const webViewInterface = getWebViewCallback(); + webViewInterface({ + nativeEvent: { + data: JSON.stringify({ + command: 'trackWebViewEvent', + event: { + eventName: 'pp', + pageTitle: 'Home', + pageUrl: 'http://localhost:9090', + referrer: 'http://refr.com', + pingXOffsetMin: 1, + pingXOffsetMax: 2, + pingYOffsetMin: 3, + pingYOffsetMax: 4, + }, + }), + }, + }); + + await tracker.flush(); + expect(requests.length).toBe(1); + + const [request] = requests; + const payload = await request?.json(); + expect(payload.data.length).toBe(1); + + const [event] = payload.data; + expect(event.e).toBe('pp'); + expect(event.url).toBe('http://localhost:9090'); + expect(event.refr).toBe('http://refr.com'); + expect(event.page).toBe('Home'); + expect(event.pp_mix).toBe('1'); + expect(event.pp_max).toBe('2'); + expect(event.pp_miy).toBe('3'); + expect(event.pp_may).toBe('4'); + }); + }); + + it('tracks tracker version and useragent', async () => { + const tracker = await newTracker({ + namespace: 'test', + appId: 'my-app', + endpoint: 'http://localhost:9090', + customFetch: mockFetch, + }); + + const webViewInterface = getWebViewCallback(); + webViewInterface({ + nativeEvent: { + data: JSON.stringify({ + command: 'trackWebViewEvent', + event: { + eventName: 'pv', + pageTitle: 'Home', + pageUrl: 'http://localhost:9090', + trackerVersion: 'wv-1.0.0', + useragent: 'Mozilla/5.0', + }, + }), + }, + }); + + await tracker.flush(); + expect(requests.length).toBe(1); + + const [request] = requests; + const payload = await request?.json(); + expect(payload.data.length).toBe(1); + + const [event] = payload.data; + expect(event.e).toBe('pv'); + expect(event.url).toBe('http://localhost:9090'); + expect(event.page).toBe('Home'); + expect(event.tv).toBe('wv-1.0.0'); + expect(event.ua).toBe('Mozilla/5.0'); + }); +});