@formo/analytics-react-native 1.0.0 → 1.0.2

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.
Files changed (63) hide show
  1. package/lib/commonjs/FormoAnalytics.js +141 -12
  2. package/lib/commonjs/FormoAnalytics.js.map +1 -1
  3. package/lib/commonjs/FormoAnalyticsProvider.js +3 -0
  4. package/lib/commonjs/FormoAnalyticsProvider.js.map +1 -1
  5. package/lib/commonjs/constants/events.js +30 -3
  6. package/lib/commonjs/constants/events.js.map +1 -1
  7. package/lib/commonjs/lib/crash/index.js +121 -0
  8. package/lib/commonjs/lib/crash/index.js.map +1 -0
  9. package/lib/commonjs/lib/event/EventFactory.js +88 -8
  10. package/lib/commonjs/lib/event/EventFactory.js.map +1 -1
  11. package/lib/commonjs/lib/lifecycle/index.js +29 -6
  12. package/lib/commonjs/lib/lifecycle/index.js.map +1 -1
  13. package/lib/commonjs/version.js +1 -1
  14. package/lib/module/FormoAnalytics.js +138 -8
  15. package/lib/module/FormoAnalytics.js.map +1 -1
  16. package/lib/module/FormoAnalyticsProvider.js +3 -0
  17. package/lib/module/FormoAnalyticsProvider.js.map +1 -1
  18. package/lib/module/constants/events.js +29 -2
  19. package/lib/module/constants/events.js.map +1 -1
  20. package/lib/module/lib/crash/index.js +117 -0
  21. package/lib/module/lib/crash/index.js.map +1 -0
  22. package/lib/module/lib/event/EventFactory.js +86 -8
  23. package/lib/module/lib/event/EventFactory.js.map +1 -1
  24. package/lib/module/lib/lifecycle/index.js +29 -6
  25. package/lib/module/lib/lifecycle/index.js.map +1 -1
  26. package/lib/module/version.js +1 -1
  27. package/lib/typescript/commonjs/FormoAnalytics.d.ts +52 -1
  28. package/lib/typescript/commonjs/FormoAnalytics.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/FormoAnalyticsProvider.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/constants/events.d.ts +23 -0
  31. package/lib/typescript/commonjs/constants/events.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/lib/crash/index.d.ts +41 -0
  33. package/lib/typescript/commonjs/lib/crash/index.d.ts.map +1 -0
  34. package/lib/typescript/commonjs/lib/event/EventFactory.d.ts +26 -7
  35. package/lib/typescript/commonjs/lib/event/EventFactory.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/lib/lifecycle/index.d.ts +10 -0
  37. package/lib/typescript/commonjs/lib/lifecycle/index.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/types/base.d.ts +40 -0
  39. package/lib/typescript/commonjs/types/base.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/version.d.ts +1 -1
  41. package/lib/typescript/module/FormoAnalytics.d.ts +52 -1
  42. package/lib/typescript/module/FormoAnalytics.d.ts.map +1 -1
  43. package/lib/typescript/module/FormoAnalyticsProvider.d.ts.map +1 -1
  44. package/lib/typescript/module/constants/events.d.ts +23 -0
  45. package/lib/typescript/module/constants/events.d.ts.map +1 -1
  46. package/lib/typescript/module/lib/crash/index.d.ts +41 -0
  47. package/lib/typescript/module/lib/crash/index.d.ts.map +1 -0
  48. package/lib/typescript/module/lib/event/EventFactory.d.ts +26 -7
  49. package/lib/typescript/module/lib/event/EventFactory.d.ts.map +1 -1
  50. package/lib/typescript/module/lib/lifecycle/index.d.ts +10 -0
  51. package/lib/typescript/module/lib/lifecycle/index.d.ts.map +1 -1
  52. package/lib/typescript/module/types/base.d.ts +40 -0
  53. package/lib/typescript/module/types/base.d.ts.map +1 -1
  54. package/lib/typescript/module/version.d.ts +1 -1
  55. package/package.json +8 -8
  56. package/src/FormoAnalytics.ts +151 -8
  57. package/src/FormoAnalyticsProvider.tsx +3 -0
  58. package/src/constants/events.ts +32 -2
  59. package/src/lib/crash/index.ts +145 -0
  60. package/src/lib/event/EventFactory.ts +115 -21
  61. package/src/lib/lifecycle/index.ts +35 -6
  62. package/src/types/base.ts +47 -0
  63. package/src/version.ts +1 -1
@@ -0,0 +1,145 @@
1
+ /**
2
+ * JavaScript crash reporting.
3
+ *
4
+ * Emits the Segment-spec `Application Crashed` event for unhandled JS errors by
5
+ * wrapping React Native's global error handler.
6
+ *
7
+ * Scope: JavaScript errors only. A native crash (a Swift/Kotlin exception, an
8
+ * OOM kill, a watchdog termination) never reaches the JS runtime, so it cannot
9
+ * be observed from here — that needs a native crash reporter.
10
+ */
11
+
12
+ import { LIFECYCLE_EVENT } from "../../constants/events";
13
+ import { logger } from "../logger";
14
+
15
+ /** RN's global handler signature. */
16
+ type ErrorHandler = (error: Error, isFatal?: boolean) => void;
17
+
18
+ interface ErrorUtilsLike {
19
+ getGlobalHandler?: () => ErrorHandler | undefined;
20
+ setGlobalHandler?: (handler: ErrorHandler) => void;
21
+ }
22
+
23
+ /** Interface for the analytics instance to avoid circular deps */
24
+ interface IAnalyticsInstance {
25
+ track(event: string, properties?: Record<string, unknown>): Promise<void>;
26
+ flush(): Promise<void>;
27
+ }
28
+
29
+ /** Stack traces can be long; cap them so one crash can't dominate a batch. */
30
+ const MAX_STACK_LENGTH = 4000;
31
+
32
+ function getErrorUtils(): ErrorUtilsLike | undefined {
33
+ // ErrorUtils is a React Native global, not an importable module. It is absent
34
+ // under plain Node (unit tests, SSR), hence the guarded lookup.
35
+ return (globalThis as { ErrorUtils?: ErrorUtilsLike }).ErrorUtils;
36
+ }
37
+
38
+ export class CrashReporter {
39
+ private analytics: IAnalyticsInstance;
40
+ private previousHandler: ErrorHandler | undefined;
41
+ private installedHandler: ErrorHandler | undefined;
42
+ private started = false;
43
+
44
+ constructor(analytics: IAnalyticsInstance) {
45
+ this.analytics = analytics;
46
+ }
47
+
48
+ /**
49
+ * Install the global error handler.
50
+ *
51
+ * The previous handler is always called afterwards — React Native's default
52
+ * handler is what shows the redbox in dev and terminates the app on a fatal
53
+ * error in production, and a customer's crash reporter (Sentry, Bugsnag) may
54
+ * also be in the chain. Swallowing it would break both.
55
+ */
56
+ start(): void {
57
+ if (this.started) return;
58
+
59
+ const errorUtils = getErrorUtils();
60
+ if (!errorUtils?.setGlobalHandler || !errorUtils?.getGlobalHandler) {
61
+ logger.debug(
62
+ "CrashReporter: ErrorUtils unavailable, skipping crash tracking",
63
+ );
64
+ return;
65
+ }
66
+
67
+ // Captured in the closure, NOT read from `this` at crash time. If another
68
+ // reporter wraps us afterwards our handler stays reachable through its
69
+ // chain, and cleanup() clearing the field would otherwise sever the chain
70
+ // at the moment it matters: A.start, B.start, A.cleanup, crash — B calls
71
+ // our handler, which would then forward to `undefined` and the real
72
+ // RN/default handler would never run.
73
+ const previousHandler = errorUtils.getGlobalHandler();
74
+ this.previousHandler = previousHandler;
75
+
76
+ const handler: ErrorHandler = (error, isFatal) => {
77
+ // Nothing in here may throw: this runs while the app is already failing,
78
+ // and an exception would replace the real crash with ours.
79
+ //
80
+ // `started` is checked so a cleaned-up reporter still FORWARDS (keeping
81
+ // the chain intact) but no longer reports — otherwise the sequence above
82
+ // would emit a duplicate Application Crashed from the stopped instance.
83
+ if (this.started) {
84
+ try {
85
+ this.report(error, isFatal);
86
+ } catch (reportingError) {
87
+ logger.debug("CrashReporter: failed to report crash", reportingError);
88
+ }
89
+ }
90
+
91
+ previousHandler?.(error, isFatal);
92
+ };
93
+
94
+ this.installedHandler = handler;
95
+ errorUtils.setGlobalHandler(handler);
96
+ this.started = true;
97
+ logger.info("CrashReporter: Started");
98
+ }
99
+
100
+ private report(error: Error, isFatal?: boolean): void {
101
+ const stack = typeof error?.stack === "string" ? error.stack : "";
102
+
103
+ // Fire and forget. On a fatal error the process is about to die, so the
104
+ // flush is a best effort — the queue also flushes on background and on the
105
+ // next launch's retry, which is where most fatal crashes are actually
106
+ // recovered from.
107
+ void this.analytics
108
+ .track(LIFECYCLE_EVENT.APPLICATION_CRASHED, {
109
+ message: error?.message ?? String(error),
110
+ name: error?.name ?? "Error",
111
+ stack: stack.slice(0, MAX_STACK_LENGTH),
112
+ stack_truncated: stack.length > MAX_STACK_LENGTH,
113
+ fatal: Boolean(isFatal),
114
+ })
115
+ .then(() => this.analytics.flush())
116
+ .catch((trackError) => {
117
+ logger.debug("CrashReporter: failed to send crash event", trackError);
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Restore the previous handler.
123
+ *
124
+ * Only restores if ours is still the installed handler — if something else
125
+ * wrapped us afterwards, replacing the chain would unhook that too.
126
+ */
127
+ cleanup(): void {
128
+ if (!this.started) return;
129
+
130
+ const errorUtils = getErrorUtils();
131
+ if (
132
+ errorUtils?.getGlobalHandler?.() === this.installedHandler &&
133
+ errorUtils?.setGlobalHandler &&
134
+ this.previousHandler
135
+ ) {
136
+ errorUtils.setGlobalHandler(this.previousHandler);
137
+ }
138
+
139
+ // previousHandler is deliberately NOT cleared: if another reporter wrapped
140
+ // us, our installed handler is still in its chain and must keep forwarding.
141
+ this.started = false;
142
+ this.installedHandler = undefined;
143
+ logger.info("CrashReporter: Cleaned up");
144
+ }
145
+ }
@@ -117,6 +117,70 @@ export function getSessionId(): string {
117
117
  * containing the tokens the classifier keys off (iphone/ipad/android + version)
118
118
  * so mobile device and OS resolve correctly. Returns "" for unknown platforms.
119
119
  */
120
+ /**
121
+ * Resolve `device_type` from expo-device's `deviceType` enum.
122
+ *
123
+ * Extracted and guarded because the Expo branch of getDeviceInfo() runs when
124
+ * EITHER expo-device or expo-application is installed, so `deviceType` may be
125
+ * undefined. A bare `deviceType === DeviceType.TABLET` is then
126
+ * `undefined === undefined` — true — and every device reports as a tablet,
127
+ * which also flips the synthesized user agent to iPad/Tablet and corrupts
128
+ * device breakdowns downstream. Unknown means "mobile", the safe default for a
129
+ * React Native app.
130
+ */
131
+ interface DeviceInfoResult {
132
+ os_name: string;
133
+ os_version: string;
134
+ device_model: string;
135
+ device_manufacturer: string;
136
+ device_name: string;
137
+ device_type: string;
138
+ user_agent: string;
139
+ app_name: string;
140
+ app_version: string;
141
+ app_build: string;
142
+ app_bundle_id: string;
143
+ }
144
+
145
+ /**
146
+ * Compose a screen view's `page_url` as `app://<bundle id>/<screen>`.
147
+ *
148
+ * Well-formed on purpose: the bundle id occupies the authority slot and the
149
+ * screen the path, so a standard URL parser yields both without any
150
+ * mobile-specific handling — the same shape as `https://<host>/<path>`.
151
+ *
152
+ * @param bundleId Application bundle id, e.g. "com.acme.wallet". When empty the
153
+ * authority is omitted (`app:///<screen>`), which still parses to a correct
154
+ * path; the pipeline resolves origin from context in that case.
155
+ * @param name Screen name. Leading slashes are stripped so a router-style name
156
+ * ("/tabs/leaderboard") does not produce a doubled separator.
157
+ */
158
+ export function buildScreenUrl(bundleId: string, name: string): string {
159
+ const screen = (name ?? "")
160
+ .replace(/^\/+/, "")
161
+ // Percent-encode the characters that would otherwise change the URL's
162
+ // STRUCTURE rather than its path. '?' starts a query and '#' a fragment, so
163
+ // screen("Checkout?coupon=X") would parse with pathname "/Checkout" and the
164
+ // rest silently dropped from the screen name. '/' is deliberately NOT
165
+ // encoded — router-style names like "/tabs/leaderboard" are meant to be
166
+ // path segments.
167
+ // '%' FIRST, so the transform stays injective. Without it a screen literally
168
+ // named "Checkout%3Fx" and one named "Checkout?x" both produce
169
+ // ".../Checkout%3Fx" and two distinct screens merge in the analytics.
170
+ .replace(/%/g, "%25")
171
+ .replace(/\?/g, "%3F")
172
+ .replace(/#/g, "%23");
173
+ return `app://${bundleId ?? ""}/${screen}`;
174
+ }
175
+
176
+ export function resolveExpoDeviceType(
177
+ deviceType: number | null | undefined,
178
+ tabletEnumValue: number | null | undefined,
179
+ ): "tablet" | "mobile" {
180
+ if (deviceType == null || tabletEnumValue == null) return "mobile";
181
+ return deviceType === tabletEnumValue ? "tablet" : "mobile";
182
+ }
183
+
120
184
  export function synthesizeUserAgent(info: {
121
185
  os_name: string;
122
186
  os_version: string;
@@ -146,6 +210,8 @@ export function synthesizeUserAgent(info: {
146
210
  */
147
211
  class EventFactory implements IEventFactory {
148
212
  private options?: Options;
213
+ /** Memoised device/app identity — see getDeviceInfo(). */
214
+ private deviceInfoPromise?: Promise<DeviceInfoResult>;
149
215
 
150
216
  constructor(options?: Options) {
151
217
  this.options = options;
@@ -269,19 +335,32 @@ class EventFactory implements IEventFactory {
269
335
  * Get device information
270
336
  * Supports both react-native-device-info (bare RN) and expo-device/expo-application (Expo Go)
271
337
  */
272
- private async getDeviceInfo(): Promise<{
273
- os_name: string;
274
- os_version: string;
275
- device_model: string;
276
- device_manufacturer: string;
277
- device_name: string;
278
- device_type: string;
279
- user_agent: string;
280
- app_name: string;
281
- app_version: string;
282
- app_build: string;
283
- app_bundle_id: string;
284
- }> {
338
+ /**
339
+ * The app bundle id as it will appear in context.
340
+ *
341
+ * Must mirror generateContext's precedence: an explicitly configured
342
+ * `options.app.bundleId` overrides whatever the native modules report. Reading
343
+ * getDeviceInfo() alone would ignore that configuration and silently fall back
344
+ * to the authority-less URL form — and on React Native Web, where neither
345
+ * react-native-device-info nor expo-application resolves a bundle id, that is
346
+ * the ONLY value available.
347
+ */
348
+ private async resolveAppBundleId(): Promise<string> {
349
+ return (
350
+ this.options?.app?.bundleId || (await this.getDeviceInfo()).app_bundle_id || ""
351
+ );
352
+ }
353
+
354
+ private async getDeviceInfo(): Promise<DeviceInfoResult> {
355
+ // Device and app identity do not change for the lifetime of the process,
356
+ // and resolving them crosses the native bridge. Memoise the promise so
357
+ // every event after the first is free, and so callers that need only the
358
+ // app identifier (screen events) can ask without paying twice.
359
+ this.deviceInfoPromise ??= this.resolveDeviceInfo();
360
+ return this.deviceInfoPromise;
361
+ }
362
+
363
+ private async resolveDeviceInfo(): Promise<DeviceInfoResult> {
285
364
  // Try react-native-device-info first (bare RN and Expo dev builds)
286
365
  if (DeviceInfo) {
287
366
  try {
@@ -324,11 +403,13 @@ class EventFactory implements IEventFactory {
324
403
  // Fall back to Expo modules (Expo Go)
325
404
  if (ExpoDevice || ExpoApplication) {
326
405
  try {
327
- const isTablet = ExpoDevice?.deviceType === ExpoDevice?.DeviceType?.TABLET;
328
406
  const os_name = ExpoDevice?.osName || Platform.OS;
329
407
  const os_version = ExpoDevice?.osVersion || String(Platform.Version);
330
408
  const device_model = ExpoDevice?.modelName || "Unknown";
331
- const device_type = isTablet ? "tablet" : "mobile";
409
+ const device_type = resolveExpoDeviceType(
410
+ ExpoDevice?.deviceType,
411
+ ExpoDevice?.DeviceType?.TABLET,
412
+ );
332
413
  return {
333
414
  os_name,
334
415
  os_version,
@@ -488,15 +569,28 @@ class EventFactory implements IEventFactory {
488
569
  const props = { ...(properties ?? {}), name, ...(category && { category }) };
489
570
 
490
571
  // Map screen name to page-equivalent context fields so mobile screens flow
491
- // through the same analytics as web page views. The screen name is emitted
492
- // as-is in the app:// URL; the ingestion pipeline derives `origin` (from the
493
- // app identifier in context app_name / app_bundle_id) and `page_path` (by
494
- // stripping the app:// scheme), so the SDK deliberately does NOT encode a
495
- // host here (see backend mobile page-event handling, P-2070).
572
+ // through the same analytics as web page views.
573
+ //
574
+ // The URL is app://<bundle id>/<screen>, which is a WELL-FORMED URL: the
575
+ // bundle id is the authority and the screen is the path, exactly mirroring
576
+ // https://<host>/<path> on web. That is what lets the ingestion pipeline
577
+ // parse it with the same URL functions it uses for web — the authority
578
+ // becomes `origin` and the path becomes `page_path`, with no mobile
579
+ // special-casing.
580
+ //
581
+ // The earlier form was app://<screen>, which is malformed: the screen name
582
+ // lands in the authority slot and there is no path at all, so a URL parser
583
+ // yields an empty path and the pipeline had to reconstruct both fields by
584
+ // hand. The bundle id is also the right choice of authority because, like a
585
+ // hostname, it is stable and globally unique — a display name is neither.
586
+ //
587
+ // Falls back to an empty authority (app:///<screen>) when the bundle id is
588
+ // unavailable, which keeps the path parseable and lets the pipeline resolve
589
+ // origin from context instead.
496
590
  // User-supplied context values take precedence (spread last).
497
591
  const screenContext: IFormoEventContext = {
498
592
  page_title: name,
499
- page_url: `app://${name}`,
593
+ page_url: buildScreenUrl(await this.resolveAppBundleId(), name),
500
594
  ...(context ?? {}),
501
595
  };
502
596
 
@@ -6,8 +6,15 @@
6
6
  * - Application Updated (version/build changed)
7
7
  * - Application Opened (every cold start + foreground return)
8
8
  * - Application Backgrounded (app goes to background)
9
+ * - Application Foregrounded (foreground return; opt-in, see AutocaptureOptions)
9
10
  *
10
11
  * Detection is JS-side using AsyncStorage (no native modules required).
12
+ *
13
+ * The remaining spec events live outside this manager because they are not
14
+ * AppState-driven: `Deep Link Opened` is emitted from the Linking hook in
15
+ * FormoAnalytics, `Application Crashed` from lib/crash, and the push
16
+ * notification events from explicit FormoAnalytics methods (the SDK cannot
17
+ * observe push delivery without a native module).
11
18
  */
12
19
 
13
20
  import { AppState, AppStateStatus, Linking } from "react-native";
@@ -18,6 +25,7 @@ import {
18
25
  LOCAL_APP_BUILD_KEY,
19
26
  } from "../../constants/storage";
20
27
  import { getStoredTrafficSource } from "../../utils/trafficSource";
28
+ import { LIFECYCLE_EVENT } from "../../constants/events";
21
29
 
22
30
  /** Interface for the analytics instance to avoid circular deps */
23
31
  interface IAnalyticsInstance {
@@ -75,6 +83,7 @@ export class AppLifecycleManager {
75
83
  private appStateSubscription: { remove: () => void } | null = null;
76
84
  private lastAppState: AppStateStatus = AppState.currentState;
77
85
  private appVersionInfo: AppVersionInfo = { version: "", build: "" };
86
+ private trackForegrounded = false;
78
87
 
79
88
  constructor(analytics: IAnalyticsInstance) {
80
89
  this.analytics = analytics;
@@ -85,8 +94,10 @@ export class AppLifecycleManager {
85
94
  * Detects install/update, fires Application Opened, and sets up AppState listener.
86
95
  */
87
96
  async start(
88
- appOptions?: { version?: string; build?: string }
97
+ appOptions?: { version?: string; build?: string },
98
+ options?: { trackForegrounded?: boolean }
89
99
  ): Promise<void> {
100
+ this.trackForegrounded = options?.trackForegrounded ?? false;
90
101
  this.appVersionInfo = await resolveAppVersionInfo(appOptions);
91
102
 
92
103
  // Detect install vs update
@@ -103,7 +114,7 @@ export class AppLifecycleManager {
103
114
  // Linking not available
104
115
  }
105
116
 
106
- await this.analytics.track("Application Opened", {
117
+ await this.analytics.track(LIFECYCLE_EVENT.APPLICATION_OPENED, {
107
118
  version: this.appVersionInfo.version,
108
119
  build: this.appVersionInfo.build,
109
120
  from_background: false,
@@ -151,7 +162,7 @@ export class AppLifecycleManager {
151
162
  Object.entries(trafficSource).filter(([, value]) => Boolean(value))
152
163
  );
153
164
  logger.info("AppLifecycleManager: Application Installed");
154
- await this.analytics.track("Application Installed", {
165
+ await this.analytics.track(LIFECYCLE_EVENT.APPLICATION_INSTALLED, {
155
166
  version,
156
167
  build,
157
168
  ...attribution,
@@ -159,7 +170,7 @@ export class AppLifecycleManager {
159
170
  } else if (previousVersion !== version || previousBuild !== build) {
160
171
  // Version or build changed — update
161
172
  logger.info("AppLifecycleManager: Application Updated");
162
- await this.analytics.track("Application Updated", {
173
+ await this.analytics.track(LIFECYCLE_EVENT.APPLICATION_UPDATED, {
163
174
  version,
164
175
  build,
165
176
  previous_version: previousVersion || "",
@@ -186,7 +197,7 @@ export class AppLifecycleManager {
186
197
  if (nextAppState === "active" && this.lastAppState === "background") {
187
198
  // Returning from background
188
199
  this.analytics
189
- .track("Application Opened", {
200
+ .track(LIFECYCLE_EVENT.APPLICATION_OPENED, {
190
201
  version: this.appVersionInfo.version,
191
202
  build: this.appVersionInfo.build,
192
203
  from_background: true,
@@ -194,12 +205,30 @@ export class AppLifecycleManager {
194
205
  .catch((error) => {
195
206
  logger.error("AppLifecycleManager: Error tracking Application Opened", error);
196
207
  });
208
+
209
+ // The Segment spec's dedicated name for this same transition. Opt-in,
210
+ // because emitting it alongside Application Opened doubles foreground
211
+ // volume for no extra information — it exists for consumers that key on
212
+ // the spec name rather than on `from_background`.
213
+ if (this.trackForegrounded) {
214
+ this.analytics
215
+ .track(LIFECYCLE_EVENT.APPLICATION_FOREGROUNDED, {
216
+ version: this.appVersionInfo.version,
217
+ build: this.appVersionInfo.build,
218
+ })
219
+ .catch((error) => {
220
+ logger.error(
221
+ "AppLifecycleManager: Error tracking Application Foregrounded",
222
+ error
223
+ );
224
+ });
225
+ }
197
226
  }
198
227
 
199
228
  if (nextAppState === "background" && this.lastAppState === "active") {
200
229
  // Going to background
201
230
  this.analytics
202
- .track("Application Backgrounded", {
231
+ .track(LIFECYCLE_EVENT.APPLICATION_BACKGROUNDED, {
203
232
  version: this.appVersionInfo.version,
204
233
  build: this.appVersionInfo.build,
205
234
  })
package/src/types/base.ts CHANGED
@@ -95,6 +95,13 @@ export interface IFormoAnalytics {
95
95
  callback?: (...args: unknown[]) => void
96
96
  ): Promise<void>;
97
97
 
98
+ // Push notification lifecycle events. Not autocaptured: push delivery is
99
+ // invisible to JavaScript without a native module, so the host app forwards
100
+ // these from its own push handler.
101
+ pushNotificationReceived(properties?: IFormoEventProperties): Promise<void>;
102
+ pushNotificationTapped(properties?: IFormoEventProperties): Promise<void>;
103
+ pushNotificationBounced(properties?: IFormoEventProperties): Promise<void>;
104
+
98
105
  // Event flushing
99
106
  flush(): Promise<void>;
100
107
 
@@ -158,6 +165,46 @@ export interface AutocaptureOptions {
158
165
  * @default true
159
166
  */
160
167
  lifecycle?: boolean;
168
+
169
+ /**
170
+ * Emit `Application Foregrounded` on every background → active transition,
171
+ * in addition to the `Application Opened` (with `from_background: true`) that
172
+ * already fires there.
173
+ *
174
+ * Off by default because it doubles foreground event volume and adds no
175
+ * information: `Application Opened` with `from_background: true` already
176
+ * marks the same transition. Enable it if you consume the Segment spec's
177
+ * `Application Foregrounded` name directly — e.g. when migrating dashboards
178
+ * or destinations from Segment or RudderStack. Requires `lifecycle`.
179
+ * @default false
180
+ */
181
+ foregrounded?: boolean;
182
+
183
+ /**
184
+ * Track `Deep Link Opened` when the app is launched or resumed via a deep
185
+ * link or universal link, with the `url` property.
186
+ *
187
+ * Independent of `attribution.deeplinks`, which controls whether the link's
188
+ * UTM/referral parameters are parsed into event context. This option is only
189
+ * about emitting the event; the SDK still needs `attribution.deeplinks` to
190
+ * observe links at all.
191
+ * @default true
192
+ */
193
+ deepLinks?: boolean;
194
+
195
+ /**
196
+ * Track `Application Crashed` on unhandled JavaScript errors, with the error
197
+ * message, stack, and whether React Native considered it fatal.
198
+ *
199
+ * Off by default because enabling it installs a global error handler
200
+ * (`ErrorUtils.setGlobalHandler`). The previous handler is always invoked
201
+ * afterwards, so React Native's redbox and any crash reporter you already use
202
+ * keep working — but installing one implicitly on an SDK upgrade is a
203
+ * surprise, so it is opt-in. Covers JS errors only: native crashes need a
204
+ * native crash reporter.
205
+ * @default false
206
+ */
207
+ crashes?: boolean;
161
208
  }
162
209
 
163
210
  /**
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // This file is auto-generated by scripts/update-version.js during npm version
2
2
  // Do not edit manually - it will be overwritten
3
- export const version = '1.0.0';
3
+ export const version = '1.0.2';