@datalyr/react-native 1.6.4 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,6 +27,7 @@ Mobile analytics and attribution SDK for React Native and Expo. Track events, id
27
27
  - [Deferred Attribution](#deferred-attribution)
28
28
  - [Customer Journey](#customer-journey)
29
29
  - [Event Queue](#event-queue)
30
+ - [Automatic Screen Tracking](#automatic-screen-tracking)
30
31
  - [Auto Events](#auto-events)
31
32
  - [SKAdNetwork](#skadnetwork)
32
33
  - [Platform Integrations](#platform-integrations)
@@ -178,10 +179,10 @@ await Datalyr.initialize({
178
179
 
179
180
  ```typescript
180
181
  interface AutoEventConfig {
181
- trackSessions?: boolean; // Track session_start / session_end
182
- trackScreenViews?: boolean; // Track screen views automatically
183
- trackAppUpdates?: boolean; // Track app_update events
184
- trackPerformance?: boolean; // Track performance metrics
182
+ trackSessions?: boolean; // Track session_start / session_end (default: true)
183
+ trackScreenViews?: boolean; // Enable screen view events via screen() (default: true)
184
+ trackAppUpdates?: boolean; // Track app_update events (default: true)
185
+ trackPerformance?: boolean; // Track performance metrics (default: false)
185
186
  sessionTimeoutMs?: number; // Session timeout in ms
186
187
  }
187
188
  ```
@@ -233,6 +234,8 @@ await Datalyr.screen('Product Details', {
233
234
  });
234
235
  ```
235
236
 
237
+ Each `screen()` call fires a single `pageview` event with the `screen` property set. Session data (`session_id`, `pageviews_in_session`, `previous_screen`) is automatically attached.
238
+
236
239
  ### E-Commerce Events
237
240
 
238
241
  Standard e-commerce events:
@@ -504,6 +507,98 @@ When the device is offline:
504
507
 
505
508
  ---
506
509
 
510
+ ## Automatic Screen Tracking
511
+
512
+ Track screen views automatically when using React Navigation (v5+/v6+). The `datalyrScreenTracking` helper wires into the navigation container and fires a `pageview` event on every route change.
513
+
514
+ ### React Navigation
515
+
516
+ ```tsx
517
+ import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
518
+ import { datalyrScreenTracking } from '@datalyr/react-native';
519
+
520
+ function App() {
521
+ const navigationRef = useNavigationContainerRef();
522
+ const screenTracking = datalyrScreenTracking(navigationRef);
523
+
524
+ return (
525
+ <NavigationContainer
526
+ ref={navigationRef}
527
+ onReady={screenTracking.onReady}
528
+ onStateChange={screenTracking.onStateChange}
529
+ >
530
+ {/* ...screens */}
531
+ </NavigationContainer>
532
+ );
533
+ }
534
+ ```
535
+
536
+ ### Expo with React Navigation
537
+
538
+ For Expo projects using React Navigation, import from the Expo entry point:
539
+
540
+ ```tsx
541
+ import { datalyrScreenTracking } from '@datalyr/react-native/expo';
542
+ ```
543
+
544
+ The API is identical to the React Navigation example above.
545
+
546
+ ### Expo Router
547
+
548
+ Expo Router does not expose a `NavigationContainer`, so automatic tracking is not available. Use `datalyr.screen()` manually in your layout files instead:
549
+
550
+ ```tsx
551
+ import { datalyr } from '@datalyr/react-native/expo';
552
+ import { usePathname } from 'expo-router';
553
+ import { useEffect } from 'react';
554
+
555
+ export default function RootLayout() {
556
+ const pathname = usePathname();
557
+
558
+ useEffect(() => {
559
+ datalyr.screen(pathname);
560
+ }, [pathname]);
561
+
562
+ return <Slot />;
563
+ }
564
+ ```
565
+
566
+ ### Configuration
567
+
568
+ You can customize screen name transforms, filtering, and property extraction:
569
+
570
+ ```typescript
571
+ const screenTracking = datalyrScreenTracking(navigationRef, {
572
+ // Clean up route names
573
+ transformScreenName: (name) => name.replace('Screen', ''),
574
+
575
+ // Skip certain screens
576
+ shouldTrackScreen: (name) => !['Splash', 'Loading'].includes(name),
577
+
578
+ // Extract route params as event properties
579
+ extractProperties: (name, params) => ({
580
+ product_id: params?.productId,
581
+ }),
582
+ });
583
+ ```
584
+
585
+ ### Advanced: Custom Tracking Function
586
+
587
+ If you need to control which SDK instance is used, use `createScreenTrackingListeners` instead:
588
+
589
+ ```typescript
590
+ import { createScreenTrackingListeners } from '@datalyr/react-native';
591
+
592
+ const { onReady, onStateChange } = createScreenTrackingListeners(
593
+ navigationRef,
594
+ (screenName, properties) => myCustomSdk.screen(screenName, properties),
595
+ );
596
+ ```
597
+
598
+ > **Note:** If you enable automatic screen tracking, avoid also calling `Datalyr.screen()` manually for the same screens, as this will produce duplicate events.
599
+
600
+ ---
601
+
507
602
  ## Auto Events
508
603
 
509
604
  Enable automatic lifecycle tracking:
@@ -45,9 +45,16 @@ export declare class AutoEventsManager {
45
45
  */
46
46
  handleAppBackground(): Promise<void>;
47
47
  /**
48
- * Track automatic pageview
48
+ * Update session counters for a screen view.
49
+ * The actual pageview event is fired by the SDK's screen() method —
50
+ * this only updates internal session state to avoid double-firing.
49
51
  */
50
- trackScreenView(screenName: string, properties?: Record<string, any>): Promise<void>;
52
+ recordScreenView(screenName: string): Promise<void>;
53
+ /**
54
+ * Get session data to enrich a pageview event.
55
+ * Called by the SDK's screen() method to attach session info.
56
+ */
57
+ getScreenViewEnrichment(): Record<string, any> | null;
51
58
  /**
52
59
  * Track app launch performance
53
60
  */
@@ -155,35 +155,42 @@ export class AutoEventsManager {
155
155
  }
156
156
  }
157
157
  /**
158
- * Track automatic pageview
158
+ * Update session counters for a screen view.
159
+ * The actual pageview event is fired by the SDK's screen() method —
160
+ * this only updates internal session state to avoid double-firing.
159
161
  */
160
- async trackScreenView(screenName, properties) {
162
+ async recordScreenView(screenName) {
161
163
  try {
162
164
  if (!this.config.trackScreenViews)
163
165
  return;
164
- // Don't track the same screen twice in a row
166
+ // Don't count the same screen twice in a row
165
167
  if (this.lastScreenName === screenName)
166
168
  return;
167
- const screenProperties = {
168
- screen_name: screenName,
169
- previous_screen: this.lastScreenName,
170
- timestamp: Date.now(),
171
- ...properties,
172
- };
173
- // Add session data if available
169
+ // Update session counters (no event fired here)
174
170
  if (this.currentSession) {
175
171
  this.currentSession.screenViews++;
176
- screenProperties.session_id = this.currentSession.sessionId;
177
- screenProperties.pageviews_in_session = this.currentSession.screenViews;
172
+ this.currentSession.lastActivity = Date.now();
178
173
  }
179
- await this.trackEvent('pageview', screenProperties);
180
174
  this.lastScreenName = screenName;
181
- debugLog('Pageview tracked:', screenName);
175
+ debugLog('Screen view counted:', screenName);
182
176
  }
183
177
  catch (error) {
184
- errorLog('Error tracking screen view:', error);
178
+ errorLog('Error updating screen view:', error);
185
179
  }
186
180
  }
181
+ /**
182
+ * Get session data to enrich a pageview event.
183
+ * Called by the SDK's screen() method to attach session info.
184
+ */
185
+ getScreenViewEnrichment() {
186
+ if (!this.currentSession)
187
+ return null;
188
+ return {
189
+ session_id: this.currentSession.sessionId,
190
+ pageviews_in_session: this.currentSession.screenViews,
191
+ previous_screen: this.lastScreenName,
192
+ };
193
+ }
187
194
  /**
188
195
  * Track app launch performance
189
196
  */
@@ -177,7 +177,7 @@ export class DatalyrSDK {
177
177
  const installData = await attributionManager.trackInstall();
178
178
  await this.track('app_install', {
179
179
  platform: Platform.OS === 'ios' || Platform.OS === 'android' ? Platform.OS : 'android',
180
- sdk_version: '1.6.1',
180
+ sdk_version: '1.7.0',
181
181
  ...installData,
182
182
  });
183
183
  }
@@ -226,11 +226,21 @@ export class DatalyrSDK {
226
226
  screen: screenName,
227
227
  ...properties,
228
228
  };
229
- await this.track('pageview', screenData);
230
- // Also notify auto-events manager for automatic screen tracking
229
+ // Enrich with session data (pageview count, previous screen) if available.
230
+ // User-provided properties take precedence over enrichment.
231
231
  if (this.autoEventsManager) {
232
- await this.autoEventsManager.trackScreenView(screenName, properties);
232
+ const enrichment = this.autoEventsManager.getScreenViewEnrichment();
233
+ if (enrichment) {
234
+ for (const [key, value] of Object.entries(enrichment)) {
235
+ if (!(key in screenData)) {
236
+ screenData[key] = value;
237
+ }
238
+ }
239
+ }
240
+ // Update session counters (does NOT fire a second event)
241
+ await this.autoEventsManager.recordScreenView(screenName);
233
242
  }
243
+ await this.track('pageview', screenData);
234
244
  }
235
245
  /**
236
246
  * Identify a user
@@ -898,7 +908,7 @@ export class DatalyrSDK {
898
908
  carrier: deviceInfo.carrier,
899
909
  network_type: getNetworkType(),
900
910
  timestamp: Date.now(),
901
- sdk_version: '1.6.1',
911
+ sdk_version: '1.7.0',
902
912
  // Advertiser data (IDFA/GAID, ATT status) for server-side postback
903
913
  ...(advertiserInfo ? {
904
914
  idfa: advertiserInfo.idfa,
package/lib/index.d.ts CHANGED
@@ -10,6 +10,8 @@ export * from './utils';
10
10
  export * from './http-client';
11
11
  export * from './event-queue';
12
12
  export { DatalyrSDK };
13
+ export { datalyrScreenTracking, createScreenTrackingListeners, } from './screen-tracking';
14
+ export type { ScreenTrackingConfig, NavigationContainerRef } from './screen-tracking';
13
15
  export { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
14
16
  export { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
15
17
  export { appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
package/lib/index.js CHANGED
@@ -16,6 +16,8 @@ export * from './http-client';
16
16
  export * from './event-queue';
17
17
  // Also export the SDK class for advanced usage
18
18
  export { DatalyrSDK };
19
+ // Export automatic screen tracking for React Navigation
20
+ export { datalyrScreenTracking, createScreenTrackingListeners, } from './screen-tracking';
19
21
  // Export SKAdNetwork components
20
22
  export { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
21
23
  export { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Automatic screen tracking for React Navigation (v5+ / v6+).
3
+ *
4
+ * The simplest integration — just spread onto your NavigationContainer:
5
+ *
6
+ * ```tsx
7
+ * import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
8
+ * import { datalyrScreenTracking } from '@datalyr/react-native';
9
+ *
10
+ * const navigationRef = useNavigationContainerRef();
11
+ * const screenTracking = datalyrScreenTracking(navigationRef);
12
+ *
13
+ * <NavigationContainer
14
+ * ref={navigationRef}
15
+ * onReady={screenTracking.onReady}
16
+ * onStateChange={screenTracking.onStateChange}
17
+ * />
18
+ * ```
19
+ *
20
+ * Note: If you enable automatic screen tracking, avoid also calling
21
+ * `Datalyr.screen()` / `datalyr.screen()` manually for the same screens,
22
+ * as this will produce duplicate events.
23
+ *
24
+ * For Expo Router (file-based routing), automatic tracking is not needed.
25
+ * Use the `datalyr.screen()` method in your layout files instead.
26
+ */
27
+ /** Minimal subset of React Navigation's NavigationContainerRef that we need. */
28
+ export interface NavigationContainerRef {
29
+ getCurrentRoute(): {
30
+ name: string;
31
+ params?: Record<string, any>;
32
+ } | undefined;
33
+ }
34
+ export interface ScreenTrackingConfig {
35
+ /**
36
+ * Transform the route name before tracking.
37
+ * Useful for cleaning up or grouping screen names.
38
+ * @example (name) => name.replace('Screen', '')
39
+ */
40
+ transformScreenName?: (routeName: string) => string;
41
+ /**
42
+ * Filter which screens should be tracked.
43
+ * Return false to skip tracking for a given screen.
44
+ * @example (name) => !['Loading', 'Splash'].includes(name)
45
+ */
46
+ shouldTrackScreen?: (routeName: string) => boolean;
47
+ /**
48
+ * Extract additional properties from the route to include in the screen event.
49
+ * @example (name, params) => ({ product_id: params?.productId })
50
+ */
51
+ extractProperties?: (routeName: string, params?: Record<string, any>) => Record<string, any>;
52
+ }
53
+ /**
54
+ * Create `onReady` and `onStateChange` callbacks that automatically
55
+ * fire screen events through the Datalyr SDK whenever the active
56
+ * React Navigation route changes.
57
+ *
58
+ * @param navigationRef A React Navigation `NavigationContainerRef`
59
+ * (from `useNavigationContainerRef()` or
60
+ * `createNavigationContainerRef()`).
61
+ * @param trackScreen The function that records a screen event.
62
+ * Pass `datalyr.screen.bind(datalyr)` or the
63
+ * Datalyr static class's `Datalyr.screen`.
64
+ * If omitted, the default singleton is used.
65
+ * @param config Optional filtering / transform config.
66
+ */
67
+ export declare function createScreenTrackingListeners(navigationRef: NavigationContainerRef, trackScreen: (screenName: string, properties?: Record<string, any>) => Promise<void>, config?: ScreenTrackingConfig): {
68
+ onReady: () => void;
69
+ onStateChange: () => void;
70
+ };
71
+ /**
72
+ * Auto-wire screen tracking to the default Datalyr singleton.
73
+ * This is the recommended API for most users.
74
+ *
75
+ * ```tsx
76
+ * const navigationRef = useNavigationContainerRef();
77
+ * const screenTracking = datalyrScreenTracking(navigationRef);
78
+ *
79
+ * <NavigationContainer
80
+ * ref={navigationRef}
81
+ * onReady={screenTracking.onReady}
82
+ * onStateChange={screenTracking.onStateChange}
83
+ * />
84
+ * ```
85
+ *
86
+ * @param navigationRef React Navigation container ref
87
+ * @param config Optional screen name transforms and filters
88
+ */
89
+ export declare function datalyrScreenTracking(navigationRef: NavigationContainerRef, config?: ScreenTrackingConfig): {
90
+ onReady: () => void;
91
+ onStateChange: () => void;
92
+ };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Automatic screen tracking for React Navigation (v5+ / v6+).
3
+ *
4
+ * The simplest integration — just spread onto your NavigationContainer:
5
+ *
6
+ * ```tsx
7
+ * import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
8
+ * import { datalyrScreenTracking } from '@datalyr/react-native';
9
+ *
10
+ * const navigationRef = useNavigationContainerRef();
11
+ * const screenTracking = datalyrScreenTracking(navigationRef);
12
+ *
13
+ * <NavigationContainer
14
+ * ref={navigationRef}
15
+ * onReady={screenTracking.onReady}
16
+ * onStateChange={screenTracking.onStateChange}
17
+ * />
18
+ * ```
19
+ *
20
+ * Note: If you enable automatic screen tracking, avoid also calling
21
+ * `Datalyr.screen()` / `datalyr.screen()` manually for the same screens,
22
+ * as this will produce duplicate events.
23
+ *
24
+ * For Expo Router (file-based routing), automatic tracking is not needed.
25
+ * Use the `datalyr.screen()` method in your layout files instead.
26
+ */
27
+ import { debugLog, errorLog } from './utils';
28
+ // ---------------------------------------------------------------------------
29
+ // Core implementation
30
+ // ---------------------------------------------------------------------------
31
+ /**
32
+ * Create `onReady` and `onStateChange` callbacks that automatically
33
+ * fire screen events through the Datalyr SDK whenever the active
34
+ * React Navigation route changes.
35
+ *
36
+ * @param navigationRef A React Navigation `NavigationContainerRef`
37
+ * (from `useNavigationContainerRef()` or
38
+ * `createNavigationContainerRef()`).
39
+ * @param trackScreen The function that records a screen event.
40
+ * Pass `datalyr.screen.bind(datalyr)` or the
41
+ * Datalyr static class's `Datalyr.screen`.
42
+ * If omitted, the default singleton is used.
43
+ * @param config Optional filtering / transform config.
44
+ */
45
+ export function createScreenTrackingListeners(navigationRef, trackScreen, config) {
46
+ let currentRouteName;
47
+ const resolveScreenName = (routeName) => (config === null || config === void 0 ? void 0 : config.transformScreenName) ? config.transformScreenName(routeName) : routeName;
48
+ const buildProperties = (routeName, params, previousRouteName) => {
49
+ const props = { source: 'auto_navigation' };
50
+ if (previousRouteName) {
51
+ props.previous_screen = resolveScreenName(previousRouteName);
52
+ }
53
+ if (config === null || config === void 0 ? void 0 : config.extractProperties) {
54
+ Object.assign(props, config.extractProperties(routeName, params));
55
+ }
56
+ return props;
57
+ };
58
+ const shouldTrack = (routeName) => !(config === null || config === void 0 ? void 0 : config.shouldTrackScreen) || config.shouldTrackScreen(routeName);
59
+ const safeTrack = (screenName, properties) => {
60
+ try {
61
+ trackScreen(screenName, properties).catch((err) => {
62
+ errorLog('Auto screen tracking failed:', err);
63
+ });
64
+ }
65
+ catch (err) {
66
+ errorLog('Auto screen tracking failed:', err);
67
+ }
68
+ };
69
+ const onReady = () => {
70
+ const route = navigationRef.getCurrentRoute();
71
+ if (!route)
72
+ return;
73
+ currentRouteName = route.name;
74
+ if (shouldTrack(route.name)) {
75
+ const screenName = resolveScreenName(route.name);
76
+ safeTrack(screenName, buildProperties(route.name, route.params));
77
+ debugLog('Auto screen tracking: initial screen', screenName);
78
+ }
79
+ };
80
+ const onStateChange = () => {
81
+ const route = navigationRef.getCurrentRoute();
82
+ if (!route)
83
+ return;
84
+ const previousRouteName = currentRouteName;
85
+ currentRouteName = route.name;
86
+ // Don't fire for same-screen param changes
87
+ if (previousRouteName === currentRouteName)
88
+ return;
89
+ if (!shouldTrack(route.name))
90
+ return;
91
+ const screenName = resolveScreenName(route.name);
92
+ safeTrack(screenName, buildProperties(route.name, route.params, previousRouteName));
93
+ debugLog('Auto screen tracking: navigated to', screenName);
94
+ };
95
+ return { onReady, onStateChange };
96
+ }
97
+ // ---------------------------------------------------------------------------
98
+ // Convenience wrapper — wires to the default Datalyr singleton
99
+ // ---------------------------------------------------------------------------
100
+ /**
101
+ * Auto-wire screen tracking to the default Datalyr singleton.
102
+ * This is the recommended API for most users.
103
+ *
104
+ * ```tsx
105
+ * const navigationRef = useNavigationContainerRef();
106
+ * const screenTracking = datalyrScreenTracking(navigationRef);
107
+ *
108
+ * <NavigationContainer
109
+ * ref={navigationRef}
110
+ * onReady={screenTracking.onReady}
111
+ * onStateChange={screenTracking.onStateChange}
112
+ * />
113
+ * ```
114
+ *
115
+ * @param navigationRef React Navigation container ref
116
+ * @param config Optional screen name transforms and filters
117
+ */
118
+ export function datalyrScreenTracking(navigationRef, config) {
119
+ // Lazily resolved reference to the SDK singleton.
120
+ // We avoid a top-level import to prevent circular deps (index.ts re-exports this file).
121
+ let sdk = null;
122
+ const getSdk = () => {
123
+ if (!sdk) {
124
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
125
+ sdk = require('./datalyr-sdk').default;
126
+ }
127
+ return sdk;
128
+ };
129
+ return createScreenTrackingListeners(navigationRef, (screenName, properties) => getSdk().screen(screenName, properties), config);
130
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalyr/react-native",
3
- "version": "1.6.4",
3
+ "version": "1.7.0",
4
4
  "description": "Datalyr SDK for React Native & Expo - Server-side attribution tracking for iOS and Android",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -195,39 +195,44 @@ export class AutoEventsManager {
195
195
  }
196
196
 
197
197
  /**
198
- * Track automatic pageview
198
+ * Update session counters for a screen view.
199
+ * The actual pageview event is fired by the SDK's screen() method —
200
+ * this only updates internal session state to avoid double-firing.
199
201
  */
200
- async trackScreenView(screenName: string, properties?: Record<string, any>): Promise<void> {
202
+ async recordScreenView(screenName: string): Promise<void> {
201
203
  try {
202
204
  if (!this.config.trackScreenViews) return;
203
-
204
- // Don't track the same screen twice in a row
205
+
206
+ // Don't count the same screen twice in a row
205
207
  if (this.lastScreenName === screenName) return;
206
-
207
- const screenProperties: Record<string, any> = {
208
- screen_name: screenName,
209
- previous_screen: this.lastScreenName,
210
- timestamp: Date.now(),
211
- ...properties,
212
- };
213
208
 
214
- // Add session data if available
209
+ // Update session counters (no event fired here)
215
210
  if (this.currentSession) {
216
211
  this.currentSession.screenViews++;
217
- screenProperties.session_id = this.currentSession.sessionId;
218
- screenProperties.pageviews_in_session = this.currentSession.screenViews;
212
+ this.currentSession.lastActivity = Date.now();
219
213
  }
220
214
 
221
- await this.trackEvent('pageview', screenProperties);
222
-
223
215
  this.lastScreenName = screenName;
224
- debugLog('Pageview tracked:', screenName);
216
+ debugLog('Screen view counted:', screenName);
225
217
 
226
218
  } catch (error) {
227
- errorLog('Error tracking screen view:', error as Error);
219
+ errorLog('Error updating screen view:', error as Error);
228
220
  }
229
221
  }
230
222
 
223
+ /**
224
+ * Get session data to enrich a pageview event.
225
+ * Called by the SDK's screen() method to attach session info.
226
+ */
227
+ getScreenViewEnrichment(): Record<string, any> | null {
228
+ if (!this.currentSession) return null;
229
+ return {
230
+ session_id: this.currentSession.sessionId,
231
+ pageviews_in_session: this.currentSession.screenViews,
232
+ previous_screen: this.lastScreenName,
233
+ };
234
+ }
235
+
231
236
  /**
232
237
  * Track app launch performance
233
238
  */
@@ -196,7 +196,7 @@ export class DatalyrSDKExpo {
196
196
  const installData = await attributionManager.trackInstall();
197
197
  await this.track('app_install', {
198
198
  platform: Platform.OS,
199
- sdk_version: '1.6.1',
199
+ sdk_version: '1.7.0',
200
200
  sdk_variant: 'expo',
201
201
  ...installData,
202
202
  });
@@ -248,11 +248,22 @@ export class DatalyrSDKExpo {
248
248
  ...properties,
249
249
  };
250
250
 
251
- await this.track('pageview', screenData);
252
-
251
+ // Enrich with session data (pageview count, previous screen) if available.
252
+ // User-provided properties take precedence over enrichment.
253
253
  if (this.autoEventsManager) {
254
- await this.autoEventsManager.trackScreenView(screenName, properties);
254
+ const enrichment = this.autoEventsManager.getScreenViewEnrichment();
255
+ if (enrichment) {
256
+ for (const [key, value] of Object.entries(enrichment)) {
257
+ if (!(key in screenData)) {
258
+ screenData[key] = value;
259
+ }
260
+ }
261
+ }
262
+ // Update session counters (does NOT fire a second event)
263
+ await this.autoEventsManager.recordScreenView(screenName);
255
264
  }
265
+
266
+ await this.track('pageview', screenData);
256
267
  }
257
268
 
258
269
  async identify(userId: string, properties?: UserProperties): Promise<void> {
@@ -781,7 +792,7 @@ export class DatalyrSDKExpo {
781
792
  carrier: deviceInfo.carrier,
782
793
  network_type: networkType,
783
794
  timestamp: Date.now(),
784
- sdk_version: '1.6.1',
795
+ sdk_version: '1.7.0',
785
796
  sdk_variant: 'expo',
786
797
  // Advertiser data (IDFA/GAID, ATT status) for server-side postback
787
798
  ...(advertiserInfo ? {
@@ -228,7 +228,7 @@ export class DatalyrSDK {
228
228
  const installData = await attributionManager.trackInstall();
229
229
  await this.track('app_install', {
230
230
  platform: Platform.OS === 'ios' || Platform.OS === 'android' ? Platform.OS : 'android',
231
- sdk_version: '1.6.1',
231
+ sdk_version: '1.7.0',
232
232
  ...installData,
233
233
  });
234
234
  }
@@ -285,12 +285,22 @@ export class DatalyrSDK {
285
285
  ...properties,
286
286
  };
287
287
 
288
- await this.track('pageview', screenData);
289
-
290
- // Also notify auto-events manager for automatic screen tracking
288
+ // Enrich with session data (pageview count, previous screen) if available.
289
+ // User-provided properties take precedence over enrichment.
291
290
  if (this.autoEventsManager) {
292
- await this.autoEventsManager.trackScreenView(screenName, properties);
291
+ const enrichment = this.autoEventsManager.getScreenViewEnrichment();
292
+ if (enrichment) {
293
+ for (const [key, value] of Object.entries(enrichment)) {
294
+ if (!(key in screenData)) {
295
+ screenData[key] = value;
296
+ }
297
+ }
298
+ }
299
+ // Update session counters (does NOT fire a second event)
300
+ await this.autoEventsManager.recordScreenView(screenName);
293
301
  }
302
+
303
+ await this.track('pageview', screenData);
294
304
  }
295
305
 
296
306
  /**
@@ -1076,7 +1086,7 @@ export class DatalyrSDK {
1076
1086
  carrier: deviceInfo.carrier,
1077
1087
  network_type: getNetworkType(),
1078
1088
  timestamp: Date.now(),
1079
- sdk_version: '1.6.1',
1089
+ sdk_version: '1.7.0',
1080
1090
  // Advertiser data (IDFA/GAID, ATT status) for server-side postback
1081
1091
  ...(advertiserInfo ? {
1082
1092
  idfa: advertiserInfo.idfa,
package/src/expo.ts CHANGED
@@ -56,6 +56,39 @@ export * from './event-queue';
56
56
  export { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
57
57
  export { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
58
58
 
59
+ // Export automatic screen tracking for React Navigation
60
+ export { createScreenTrackingListeners } from './screen-tracking';
61
+ export type { ScreenTrackingConfig, NavigationContainerRef } from './screen-tracking';
62
+
63
+ // Expo-specific convenience: auto-wires to the Expo singleton
64
+ import { createScreenTrackingListeners as _createListeners } from './screen-tracking';
65
+ import type { NavigationContainerRef as _NavRef, ScreenTrackingConfig as _Config } from './screen-tracking';
66
+
67
+ /**
68
+ * Auto-wire screen tracking to the Expo Datalyr singleton.
69
+ *
70
+ * ```tsx
71
+ * const navigationRef = useNavigationContainerRef();
72
+ * const screenTracking = datalyrScreenTracking(navigationRef);
73
+ *
74
+ * <NavigationContainer
75
+ * ref={navigationRef}
76
+ * onReady={screenTracking.onReady}
77
+ * onStateChange={screenTracking.onStateChange}
78
+ * />
79
+ * ```
80
+ */
81
+ export function datalyrScreenTracking(
82
+ navigationRef: _NavRef,
83
+ config?: _Config,
84
+ ): { onReady: () => void; onStateChange: () => void } {
85
+ return _createListeners(
86
+ navigationRef,
87
+ (screenName, properties) => datalyrExpo.screen(screenName, properties),
88
+ config,
89
+ );
90
+ }
91
+
59
92
  // Export platform integrations
60
93
  export { appleSearchAdsIntegration } from './integrations';
61
94
  export type { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
package/src/index.ts CHANGED
@@ -24,6 +24,13 @@ export * from './event-queue';
24
24
  // Also export the SDK class for advanced usage
25
25
  export { DatalyrSDK };
26
26
 
27
+ // Export automatic screen tracking for React Navigation
28
+ export {
29
+ datalyrScreenTracking,
30
+ createScreenTrackingListeners,
31
+ } from './screen-tracking';
32
+ export type { ScreenTrackingConfig, NavigationContainerRef } from './screen-tracking';
33
+
27
34
  // Export SKAdNetwork components
28
35
  export { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
29
36
  export { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Automatic screen tracking for React Navigation (v5+ / v6+).
3
+ *
4
+ * The simplest integration — just spread onto your NavigationContainer:
5
+ *
6
+ * ```tsx
7
+ * import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
8
+ * import { datalyrScreenTracking } from '@datalyr/react-native';
9
+ *
10
+ * const navigationRef = useNavigationContainerRef();
11
+ * const screenTracking = datalyrScreenTracking(navigationRef);
12
+ *
13
+ * <NavigationContainer
14
+ * ref={navigationRef}
15
+ * onReady={screenTracking.onReady}
16
+ * onStateChange={screenTracking.onStateChange}
17
+ * />
18
+ * ```
19
+ *
20
+ * Note: If you enable automatic screen tracking, avoid also calling
21
+ * `Datalyr.screen()` / `datalyr.screen()` manually for the same screens,
22
+ * as this will produce duplicate events.
23
+ *
24
+ * For Expo Router (file-based routing), automatic tracking is not needed.
25
+ * Use the `datalyr.screen()` method in your layout files instead.
26
+ */
27
+
28
+ import { debugLog, errorLog } from './utils';
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Minimal React Navigation types — avoids adding @react-navigation as a
32
+ // peer dependency. Only the methods we actually call are listed here.
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /** Minimal subset of React Navigation's NavigationContainerRef that we need. */
36
+ export interface NavigationContainerRef {
37
+ getCurrentRoute(): { name: string; params?: Record<string, any> } | undefined;
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Configuration
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export interface ScreenTrackingConfig {
45
+ /**
46
+ * Transform the route name before tracking.
47
+ * Useful for cleaning up or grouping screen names.
48
+ * @example (name) => name.replace('Screen', '')
49
+ */
50
+ transformScreenName?: (routeName: string) => string;
51
+
52
+ /**
53
+ * Filter which screens should be tracked.
54
+ * Return false to skip tracking for a given screen.
55
+ * @example (name) => !['Loading', 'Splash'].includes(name)
56
+ */
57
+ shouldTrackScreen?: (routeName: string) => boolean;
58
+
59
+ /**
60
+ * Extract additional properties from the route to include in the screen event.
61
+ * @example (name, params) => ({ product_id: params?.productId })
62
+ */
63
+ extractProperties?: (routeName: string, params?: Record<string, any>) => Record<string, any>;
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Core implementation
68
+ // ---------------------------------------------------------------------------
69
+
70
+ /**
71
+ * Create `onReady` and `onStateChange` callbacks that automatically
72
+ * fire screen events through the Datalyr SDK whenever the active
73
+ * React Navigation route changes.
74
+ *
75
+ * @param navigationRef A React Navigation `NavigationContainerRef`
76
+ * (from `useNavigationContainerRef()` or
77
+ * `createNavigationContainerRef()`).
78
+ * @param trackScreen The function that records a screen event.
79
+ * Pass `datalyr.screen.bind(datalyr)` or the
80
+ * Datalyr static class's `Datalyr.screen`.
81
+ * If omitted, the default singleton is used.
82
+ * @param config Optional filtering / transform config.
83
+ */
84
+ export function createScreenTrackingListeners(
85
+ navigationRef: NavigationContainerRef,
86
+ trackScreen: (screenName: string, properties?: Record<string, any>) => Promise<void>,
87
+ config?: ScreenTrackingConfig,
88
+ ): { onReady: () => void; onStateChange: () => void } {
89
+ let currentRouteName: string | undefined;
90
+
91
+ const resolveScreenName = (routeName: string): string =>
92
+ config?.transformScreenName ? config.transformScreenName(routeName) : routeName;
93
+
94
+ const buildProperties = (
95
+ routeName: string,
96
+ params?: Record<string, any>,
97
+ previousRouteName?: string,
98
+ ): Record<string, any> => {
99
+ const props: Record<string, any> = { source: 'auto_navigation' };
100
+
101
+ if (previousRouteName) {
102
+ props.previous_screen = resolveScreenName(previousRouteName);
103
+ }
104
+
105
+ if (config?.extractProperties) {
106
+ Object.assign(props, config.extractProperties(routeName, params));
107
+ }
108
+
109
+ return props;
110
+ };
111
+
112
+ const shouldTrack = (routeName: string): boolean =>
113
+ !config?.shouldTrackScreen || config.shouldTrackScreen(routeName);
114
+
115
+ const safeTrack = (screenName: string, properties: Record<string, any>) => {
116
+ try {
117
+ trackScreen(screenName, properties).catch((err) => {
118
+ errorLog('Auto screen tracking failed:', err as Error);
119
+ });
120
+ } catch (err) {
121
+ errorLog('Auto screen tracking failed:', err as Error);
122
+ }
123
+ };
124
+
125
+ const onReady = () => {
126
+ const route = navigationRef.getCurrentRoute();
127
+ if (!route) return;
128
+
129
+ currentRouteName = route.name;
130
+
131
+ if (shouldTrack(route.name)) {
132
+ const screenName = resolveScreenName(route.name);
133
+ safeTrack(screenName, buildProperties(route.name, route.params));
134
+ debugLog('Auto screen tracking: initial screen', screenName);
135
+ }
136
+ };
137
+
138
+ const onStateChange = () => {
139
+ const route = navigationRef.getCurrentRoute();
140
+ if (!route) return;
141
+
142
+ const previousRouteName = currentRouteName;
143
+ currentRouteName = route.name;
144
+
145
+ // Don't fire for same-screen param changes
146
+ if (previousRouteName === currentRouteName) return;
147
+
148
+ if (!shouldTrack(route.name)) return;
149
+
150
+ const screenName = resolveScreenName(route.name);
151
+ safeTrack(screenName, buildProperties(route.name, route.params, previousRouteName));
152
+ debugLog('Auto screen tracking: navigated to', screenName);
153
+ };
154
+
155
+ return { onReady, onStateChange };
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Convenience wrapper — wires to the default Datalyr singleton
160
+ // ---------------------------------------------------------------------------
161
+
162
+ /**
163
+ * Auto-wire screen tracking to the default Datalyr singleton.
164
+ * This is the recommended API for most users.
165
+ *
166
+ * ```tsx
167
+ * const navigationRef = useNavigationContainerRef();
168
+ * const screenTracking = datalyrScreenTracking(navigationRef);
169
+ *
170
+ * <NavigationContainer
171
+ * ref={navigationRef}
172
+ * onReady={screenTracking.onReady}
173
+ * onStateChange={screenTracking.onStateChange}
174
+ * />
175
+ * ```
176
+ *
177
+ * @param navigationRef React Navigation container ref
178
+ * @param config Optional screen name transforms and filters
179
+ */
180
+ export function datalyrScreenTracking(
181
+ navigationRef: NavigationContainerRef,
182
+ config?: ScreenTrackingConfig,
183
+ ): { onReady: () => void; onStateChange: () => void } {
184
+ // Lazily resolved reference to the SDK singleton.
185
+ // We avoid a top-level import to prevent circular deps (index.ts re-exports this file).
186
+ let sdk: { screen: (name: string, props?: Record<string, any>) => Promise<void> } | null = null;
187
+
188
+ const getSdk = () => {
189
+ if (!sdk) {
190
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
191
+ sdk = (require('./datalyr-sdk') as { default: typeof sdk }).default;
192
+ }
193
+ return sdk!;
194
+ };
195
+
196
+ return createScreenTrackingListeners(
197
+ navigationRef,
198
+ (screenName, properties) => getSdk().screen(screenName, properties),
199
+ config,
200
+ );
201
+ }