@obsrviq/react-native 0.3.1 → 0.3.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.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @obsrviq/react-native
2
2
 
3
- Mobile session replay + analytics capture for **React Native (iOS + Android)**, feeding the same Lumera backend as the web tracker. Engineered around one non-negotiable: **zero added latency on the device.**
3
+ Mobile session replay + analytics capture for **React Native (iOS + Android)**, feeding the same Obsrviq backend as the web tracker. Engineered around one non-negotiable: **zero added latency on the device.**
4
4
 
5
- > **Status: in development.** The pure-JS layer (public API, session/identity, network/error/console capture, transport) is implemented. The native screenshot-capture TurboModule (iOS Swift / Android Kotlin), the backend frame-storage path, and the mobile player renderer are in progress — see [Roadmap](#roadmap).
5
+ > **Status: shipped.** The full pipeline is live — the pure-JS layer (public API, session/identity, network/error/console capture, transport), the native screenshot-capture TurboModule (iOS Swift / Android Kotlin), native touch capture, the backend frame-storage path, and the mobile player renderer (screenshot timeline + touch ripples + network waterfall). Native code is compiled inside the consuming app's iOS/Android build — see [Building](#building-native).
6
6
 
7
7
  ---
8
8
 
@@ -59,29 +59,55 @@ Requires React Native ≥ 0.76 (New Architecture). Works under the old architect
59
59
  ## Quickstart
60
60
 
61
61
  ```ts
62
- import { LumeraReplay } from '@obsrviq/react-native';
62
+ import { ObsrviqReplay } from '@obsrviq/react-native';
63
63
 
64
- LumeraReplay.init({
64
+ ObsrviqReplay.init({
65
65
  siteKey: 'pk_live_…',
66
- // privacy-by-default: all text + images masked. Opt out per-view with <LumeraMask unmask>.
66
+ // privacy-by-default: all text + images masked. Opt out per-view with <ObsrviqMask unmask>.
67
67
  });
68
68
 
69
69
  // later — identical API to the web SDK:
70
- LumeraReplay.identify('user_123', { plan: 'pro' });
71
- LumeraReplay.track('checkout_started', { cart: 3 });
72
- LumeraReplay.conversion('purchase', { value: 49.0 });
73
- LumeraReplay.tag('vip', 'beta');
70
+ ObsrviqReplay.identify('user_123', { plan: 'pro' });
71
+ ObsrviqReplay.track('checkout_started', { cart: 3 });
72
+ ObsrviqReplay.conversion('purchase', { value: 49.0 });
73
+ ObsrviqReplay.tag('vip', 'beta');
74
74
  ```
75
75
 
76
+ > **Naming:** the SDK is `ObsrviqReplay` / `ObsrviqConfig` / `<ObsrviqMask>`. The old
77
+ > `LumeraReplay` / `LumeraConfig` / `<LumeraMask>` names are still exported as **deprecated
78
+ > aliases**, so pre-rebrand integrations keep working unchanged.
79
+
76
80
  ## API
77
81
 
78
- Mirrors `@obsrviq/tracker` 1:1 — `init`, `identify`, `setMetadata`, `track`, `conversion`, `tag`, `startTask`/`endTask`, `setConsent`, `reset`, `stop`, `flush`, `getDiagnostics`, `sessionId`. See [`src/config.ts`](src/config.ts) for the full config.
82
+ Mirrors `@obsrviq/tracker` 1:1 — `init`, `identify`, `setMetadata`, `track`, `conversion`, `tag`, `startTask`/`endTask`, `setConsent`, `reset`, `stop`, `flush`, `getDiagnostics`, `sessionId`. `screen(name)` records a route view (the mobile analog of a web pageview — call it from your navigation listener).
83
+
84
+ ## Config
85
+
86
+ Passed to `init({ siteKey, … })`. See [`src/config.ts`](src/config.ts) for the authoritative list.
87
+
88
+ | Option | Default | What it does |
89
+ |---|---|---|
90
+ | `siteKey` | — | **Required.** Your ingest key (`pk_…`). |
91
+ | `enableReplay` | `true` | Record the screen (native screenshot stream). |
92
+ | `fps` | `1` | Capture cadence cap, frames/sec (change-driven underneath). |
93
+ | `jpegQuality` | `0.4` | JPEG quality 0..1 on the encode path. |
94
+ | `maxCaptureDim` | `1200` | Cap the longer edge of each frame (px) — the storage/bandwidth lever. `0` = full native res. |
95
+ | `maskAllText` | `true` | Mask all text before any pixel is persisted. |
96
+ | `maskAllImages` | `true` | Mask all images/media. |
97
+ | `captureTouches` | `true` | Capture taps + swipes as a touch overlay (native, non-blocking gesture observer — never interferes with the app's own gestures). |
98
+ | `captureNetworkBodies` | `false` | Capture request + response bodies (masked, size-capped at 8KB). Off by default — bodies can hold PII. |
99
+ | `redactHeaders` | `['authorization','cookie','set-cookie']` | Header allowlist redaction for captured network events. |
100
+ | `captureConsole` | `true` | Mirror `console.*` into the session. |
101
+ | `sampleRate` | `1` | Fraction of sessions recorded (0..1). |
102
+ | `continueSession` | `false` | Resume the same session across foreground/background within the window. |
103
+ | `requireConsent` | `false` | Gate all recording until `setConsent(true)`. |
79
104
 
80
105
  ## Privacy / masking
81
106
 
82
107
  - **Default:** all text and images are masked before any pixel is persisted (masking runs on the capture thread, pre-encode).
83
- - **Per-view control:** `<LumeraMask>` / `<LumeraMask unmask>` wrappers (call `setViewMasked(reactTag, …)` natively). *(component pending — task #95/#96)*
84
- - Network header redaction (`authorization`/`cookie`/`set-cookie` by default), bodies off by default.
108
+ - **Per-view control:** `<ObsrviqMask>` / `<ObsrviqMask unmask>` wrappers (call `setViewMasked(reactTag, …)` natively).
109
+ - Network: header allowlist redaction (`authorization`/`cookie`/`set-cookie` by default). Request/response **bodies are off by default** — opt in with `captureNetworkBodies: true` (bodies are size-capped at 8KB and can hold personal data, so enable deliberately).
110
+ - Touch capture records only coordinates + phase (start/move/end) — never the content under the finger.
85
111
 
86
112
  ## Data flow
87
113
 
@@ -91,18 +117,21 @@ Two independent uploaders, one session:
91
117
  |---|---|---|---|---|
92
118
  | Structured (network/error/console/custom) | JS | `POST /v1/batch` | `network`/`error`/`console`/`custom` | Postgres `events` |
93
119
  | Screenshot frames | **Native** | `POST /v1/batch` | `screen` | blob + `mobile_frames` index |
120
+ | Touch (taps/swipes) | **Native** | `POST /v1/batch` | `touch` | Postgres `events` (synced to the timeline by `t`) |
94
121
 
95
- Both share `siteKey` + `sessionId`. No `seq` collision: structured events never write replay chunks; `screen` frames get their own frame index. The ingest endpoint already accepts new event types (permissive validation), so **no ingest change is needed** only a worker routing rule for `type:'screen'`.
122
+ All three share `siteKey` + `sessionId`. No `seq` collision: structured events never write replay chunks; `screen` frames get their own frame index; `touch` events are small and land in the `events` table. The worker routes `screen` to the blob store + `mobile_frames`, keeps everything else (including `touch`) in `events`. The player overlays `touch` events as ripples and renders `network` events as a per-request waterfall with an expandable headers/body inspector.
96
123
 
97
124
  ## Roadmap
98
125
 
99
126
  - [x] JS core — public API, session/identity, network/error/console capture, transport (`src/`)
100
- - [x] Wire format — `MobileScreenEvent` (`type:'screen'`) + `MobileTouchEvent` in `@obsrviq/types`
101
- - [ ] Native iOS capture (Swift TurboModule) — `ios/` *(task #95)*
102
- - [ ] Native Android capture (Kotlin TurboModule) — `android/` *(task #96)*
103
- - [ ] Backend — route `type:'screen'` to blob + `mobile_frames`, migration *(task #97)*
104
- - [ ] Player — mobile screenshot renderer + device-based routing *(task #98)*
127
+ - [x] Wire format — `MobileScreenEvent` (`type:'screen'`) + `MobileTouchEvent` (`type:'touch'`) in `@obsrviq/types`
128
+ - [x] Native iOS capture (Swift TurboModule) — screenshots + touch observer — `ios/`
129
+ - [x] Native Android capture (Kotlin TurboModule) — screenshots + touch observer — `android/`
130
+ - [x] Backend — route `type:'screen'` to blob + `mobile_frames`, `touch` to `events` (migration 017)
131
+ - [x] Player — mobile screenshot renderer + device-based routing + touch ripples + network waterfall/inspector
132
+ - [x] `<ObsrviqMask>` / `<ObsrviqMask unmask>` per-view privacy component (iOS + Android)
133
+ - [x] Network request/response **body** capture (opt-in, `captureNetworkBodies`)
105
134
 
106
135
  ## Building (native)
107
136
 
108
- Native code can't be compiled in the SDK repo — build it inside a real RN app and test on devices (jank only shows on hardware, especially low-end). Codegen runs automatically during the app's iOS/Android build from [`src/spec/NativeLumeraReplay.ts`](src/spec/NativeLumeraReplay.ts).
137
+ Native code can't be compiled in the SDK repo — build it inside a real RN app and test on devices (jank only shows on hardware, especially low-end). Codegen runs automatically during the app's iOS/Android build from [`src/spec/NativeObsrviqReplay.ts`](src/spec/NativeObsrviqReplay.ts).
@@ -8,7 +8,7 @@ apply plugin: "org.jetbrains.kotlin.android" // REQUIRED: sources are Kotlin
8
8
  apply plugin: "com.facebook.react" // enables New-Arch codegen for the TurboModule
9
9
 
10
10
  android {
11
- namespace "app.lumera.replay"
11
+ namespace "app.obsrviq.replay"
12
12
  compileSdkVersion safeExtGet("compileSdkVersion", 35)
13
13
 
14
14
  defaultConfig {
@@ -0,0 +1,42 @@
1
+ package app.obsrviq.replay
2
+
3
+ import android.view.View
4
+ import com.facebook.react.module.annotations.ReactModule
5
+ import com.facebook.react.uimanager.SimpleViewManager
6
+ import com.facebook.react.uimanager.ThemedReactContext
7
+
8
+ /**
9
+ * Backs the optional `<ObsrviqMask>` JS component. It is a plain pass-through container
10
+ * (children render normally); its sole job is to tell the capture engine "redact my
11
+ * subtree". On attach it registers its own View.id in [ObsrviqReplayModule.maskedTags];
12
+ * on detach it removes it. This is the geometry [ObsrviqReplayModule.collectMaskRects]
13
+ * walks: an explicitly-masked container yields one redaction block over the whole subtree.
14
+ *
15
+ * Kept deliberately minimal — no custom props, no shadow node. JS can also drive masking
16
+ * imperatively via `NativeObsrviqReplay.setViewMasked(reactTag, masked)` without this view.
17
+ */
18
+ @ReactModule(name = ObsrviqMaskViewManager.REACT_CLASS)
19
+ class ObsrviqMaskViewManager : SimpleViewManager<View>() {
20
+
21
+ override fun getName() = REACT_CLASS
22
+
23
+ override fun createViewInstance(reactContext: ThemedReactContext): View =
24
+ object : View(reactContext) {
25
+ override fun onAttachedToWindow() {
26
+ super.onAttachedToWindow()
27
+ ObsrviqReplayModule.maskTag(id)
28
+ }
29
+
30
+ override fun onDetachedFromWindow() {
31
+ ObsrviqReplayModule.unmaskTag(id)
32
+ super.onDetachedFromWindow()
33
+ }
34
+ }
35
+
36
+ companion object {
37
+ // Must match the JS requireNativeComponent name + the iOS RCT_EXPORT_MODULE name.
38
+ // (Was "LumeraMask" on Android while iOS/JS used "LumeraMaskView" — a name mismatch
39
+ // that silently disabled the mask component on Android. Unified to "ObsrviqMaskView".)
40
+ const val REACT_CLASS = "ObsrviqMaskView"
41
+ }
42
+ }
@@ -1,4 +1,4 @@
1
- package app.lumera.replay
1
+ package app.obsrviq.replay
2
2
 
3
3
  import android.graphics.Bitmap
4
4
  import android.graphics.Canvas
@@ -30,7 +30,7 @@ import java.util.UUID
30
30
  import java.util.concurrent.atomic.AtomicBoolean
31
31
 
32
32
  /**
33
- * Lumera native screenshot-replay capture engine (Android).
33
+ * Obsrviq native screenshot-replay capture engine (Android).
34
34
  *
35
35
  * THREADING CONTRACT:
36
36
  * • UI thread → only schedules a PixelCopy (which is async) + a cheap mask-rect walk.
@@ -41,14 +41,14 @@ import java.util.concurrent.atomic.AtomicBoolean
41
41
  * which misses hardware-accelerated content. One reused RGB_565 bitmap. No pixels
42
42
  * cross the RN bridge: this engine uploads frames itself to `${endpoint}/v1/batch`.
43
43
  *
44
- * NOTE: extends the codegen'd `NativeLumeraReplaySpec` (New Arch). The
45
- * `LumeraReplayPackage` registers it as the TurboModule named "LumeraReplay".
44
+ * NOTE: extends the codegen'd `NativeObsrviqReplaySpec` (New Arch). The
45
+ * `ObsrviqReplayPackage` registers it as the TurboModule named "ObsrviqReplay".
46
46
  */
47
- class LumeraReplayModule(private val reactCtx: ReactApplicationContext) :
48
- NativeLumeraReplaySpec(reactCtx) {
47
+ class ObsrviqReplayModule(private val reactCtx: ReactApplicationContext) :
48
+ NativeObsrviqReplaySpec(reactCtx) {
49
49
 
50
50
  // Dedicated background thread for ALL post-capture work (PixelCopy callback + encode + upload).
51
- private val bgThread = HandlerThread("lumera-replay").apply { start() }
51
+ private val bgThread = HandlerThread("obsrviq-replay").apply { start() }
52
52
  private val bg = Handler(bgThread.looper)
53
53
 
54
54
  private var opts: ReadableStartOptions? = null
@@ -81,7 +81,7 @@ class LumeraReplayModule(private val reactCtx: ReactApplicationContext) :
81
81
  private var framesDropped = 0
82
82
  private var bytesSent = 0L
83
83
 
84
- override fun getName() = "LumeraReplay"
84
+ override fun getName() = "ObsrviqReplay"
85
85
 
86
86
  // MARK: - Lifecycle
87
87
 
@@ -323,7 +323,7 @@ class LumeraReplayModule(private val reactCtx: ReactApplicationContext) :
323
323
  * Walk the decorView on the UI thread → redaction rects (in captured-pixel space) for
324
324
  * masked nodes: TextView/EditText when [ReadableStartOptions.maskAllText], ImageView when
325
325
  * [ReadableStartOptions.maskAllImages]. A view whose id (RN reactTag) is in [maskedTags]
326
- * — set via `setViewMasked(tag, true)` or `<LumeraMask>` — is force-masked along with its
326
+ * — set via `setViewMasked(tag, true)` or `<ObsrviqMask>` — is force-masked along with its
327
327
  * whole subtree (one block), regardless of mask-all. Rect = getLocationInWindow + width/height.
328
328
  *
329
329
  * Honoring per-view UN-masking under mask-all (force-unmask) would need a second, explicit
@@ -397,7 +397,7 @@ class LumeraReplayModule(private val reactCtx: ReactApplicationContext) :
397
397
 
398
398
  /**
399
399
  * Assemble ONE `IngestBatch` from buffered `screen` events and POST it to
400
- * `${endpoint}/v1/batch` (HttpURLConnection on `bg`, x-lumera-key header, plain JSON).
400
+ * `${endpoint}/v1/batch` (HttpURLConnection on `bg`, x-obsrviq-key header, plain JSON).
401
401
  * On HTTP 2xx: bump counters + ++seq. On failure: re-queue (prepend so order holds).
402
402
  */
403
403
  private fun flush() {
@@ -462,7 +462,7 @@ class LumeraReplayModule(private val reactCtx: ReactApplicationContext) :
462
462
  // octet-stream, NOT application/json — the ingest reads the raw body buffer and
463
463
  // sniffs gzip itself; application/json hits Fastify's JSON parser → 400 empty_body.
464
464
  setRequestProperty("content-type", "application/octet-stream")
465
- setRequestProperty("x-lumera-key", o.siteKey)
465
+ setRequestProperty("x-obsrviq-key", o.siteKey)
466
466
  setFixedLengthStreamingMode(payload.size)
467
467
  }
468
468
  conn.outputStream.use { os: OutputStream -> os.write(payload) }
@@ -517,8 +517,8 @@ class LumeraReplayModule(private val reactCtx: ReactApplicationContext) :
517
517
  companion object {
518
518
  /**
519
519
  * Process-wide set of explicitly-masked View ids (RN reactTags). Shared so both the
520
- * TurboModule (`setViewMasked`) and the `<LumeraMask>` view manager
521
- * ([LumeraMaskViewManager]) feed the same registry the capture walk reads. A
520
+ * TurboModule (`setViewMasked`) and the `<ObsrviqMask>` view manager
521
+ * ([ObsrviqMaskViewManager]) feed the same registry the capture walk reads. A
522
522
  * ConcurrentHashMap-backed set: written from the UI thread, read on `bg`.
523
523
  */
524
524
  private val maskedTags: MutableSet<Int> =
@@ -1,4 +1,4 @@
1
- package app.lumera.replay
1
+ package app.obsrviq.replay
2
2
 
3
3
  import com.facebook.react.BaseReactPackage
4
4
  import com.facebook.react.bridge.NativeModule
@@ -8,30 +8,30 @@ import com.facebook.react.module.model.ReactModuleInfoProvider
8
8
  import com.facebook.react.uimanager.ViewManager
9
9
 
10
10
  /**
11
- * TurboModule package for the Lumera native capture engine.
11
+ * TurboModule package for the Obsrviq native capture engine.
12
12
  *
13
13
  * Registered by the host app (autolinking via react-native.config.js, or manually in
14
14
  * MainApplication's `getPackages()`). Exposes the New-Architecture TurboModule named
15
- * "LumeraReplay" plus the optional <LumeraMask> view manager.
15
+ * "ObsrviqReplay" plus the optional <ObsrviqMask> view manager.
16
16
  *
17
17
  * Uses [BaseReactPackage] (RN 0.76+) — `getModule(name, ctx)` is resolved lazily by the
18
18
  * TurboModule infrastructure, and [getReactModuleInfoProvider] marks the module
19
19
  * `isTurboModule = true` so it is loaded over JSI (never the legacy bridge).
20
20
  */
21
- class LumeraReplayPackage : BaseReactPackage() {
21
+ class ObsrviqReplayPackage : BaseReactPackage() {
22
22
 
23
23
  override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
24
24
  when (name) {
25
- LumeraReplayModuleName -> LumeraReplayModule(reactContext)
25
+ ObsrviqReplayModuleName -> ObsrviqReplayModule(reactContext)
26
26
  else -> null
27
27
  }
28
28
 
29
29
  override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
30
30
  ReactModuleInfoProvider {
31
31
  mapOf(
32
- LumeraReplayModuleName to ReactModuleInfo(
33
- /* name = */ LumeraReplayModuleName,
34
- /* className = */ LumeraReplayModule::class.java.name,
32
+ ObsrviqReplayModuleName to ReactModuleInfo(
33
+ /* name = */ ObsrviqReplayModuleName,
34
+ /* className = */ ObsrviqReplayModule::class.java.name,
35
35
  /* canOverrideExistingModule = */ false,
36
36
  /* needsEagerInit = */ false,
37
37
  /* isCxxModule = */ false,
@@ -40,11 +40,11 @@ class LumeraReplayPackage : BaseReactPackage() {
40
40
  )
41
41
  }
42
42
 
43
- // View managers are not TurboModules; supply the optional <LumeraMask> manager directly.
43
+ // View managers are not TurboModules; supply the optional <ObsrviqMask> manager directly.
44
44
  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
45
- listOf(LumeraMaskViewManager())
45
+ listOf(ObsrviqMaskViewManager())
46
46
 
47
47
  private companion object {
48
- const val LumeraReplayModuleName = "LumeraReplay"
48
+ const val ObsrviqReplayModuleName = "ObsrviqReplay"
49
49
  }
50
50
  }
@@ -3,7 +3,7 @@
3
3
  NS_ASSUME_NONNULL_BEGIN
4
4
 
5
5
  /**
6
- * Backing native view for the <LumeraMask> RN component. It is a plain pass-through
6
+ * Backing native view for the <ObsrviqMask> RN component. It is a plain pass-through
7
7
  * container (children render normally); its only job is to register/unregister its RN
8
8
  * `reactTag` with the capture engine so the region it occupies is masked (or, with
9
9
  * `masked={false}`, explicitly UN-masked — overriding the global maskAll* defaults).
@@ -11,7 +11,7 @@ NS_ASSUME_NONNULL_BEGIN
11
11
  * The mask itself is composited natively off the UI thread during capture — this view
12
12
  * adds no per-frame cost and never rasterizes anything.
13
13
  */
14
- @interface LumeraMaskView : UIView
14
+ @interface ObsrviqMaskView : UIView
15
15
  /** YES → mask this subtree; NO → force-unmask it. Default YES. */
16
16
  @property (nonatomic, assign) BOOL masked;
17
17
  @end
@@ -1,22 +1,22 @@
1
- #import "LumeraMaskView.h"
1
+ #import "ObsrviqMaskView.h"
2
2
  #import <React/RCTComponent.h>
3
3
 
4
- // The capture engine's @objc surface (see LumeraReplay.swift). Mirrors the umbrella-
5
- // header resolution in LumeraReplayModule.mm; we only need `shared` + setViewMasked here.
6
- #if __has_include(<lumera_react_native/lumera_react_native-Swift.h>)
7
- #import <lumera_react_native/lumera_react_native-Swift.h>
8
- #elif __has_include("lumera_react_native-Swift.h")
9
- #import "lumera_react_native-Swift.h"
10
- #elif __has_include("LumeraReactNative-Swift.h")
11
- #import "LumeraReactNative-Swift.h"
4
+ // The capture engine's @objc surface (see ObsrviqReplay.swift). Mirrors the umbrella-
5
+ // header resolution in ObsrviqReplayModule.mm; we only need `shared` + setViewMasked here.
6
+ #if __has_include(<obsrviq_react_native/obsrviq_react_native-Swift.h>)
7
+ #import <obsrviq_react_native/obsrviq_react_native-Swift.h>
8
+ #elif __has_include("obsrviq_react_native-Swift.h")
9
+ #import "obsrviq_react_native-Swift.h"
10
+ #elif __has_include("ObsrviqReactNative-Swift.h")
11
+ #import "ObsrviqReactNative-Swift.h"
12
12
  #else
13
- @interface LumeraReplay : NSObject
14
- @property (class, nonatomic, readonly, strong) LumeraReplay *shared;
13
+ @interface ObsrviqReplay : NSObject
14
+ @property (class, nonatomic, readonly, strong) ObsrviqReplay *shared;
15
15
  - (void)setViewMasked:(NSNumber *)reactTag masked:(NSNumber *)masked;
16
16
  @end
17
17
  #endif
18
18
 
19
- @implementation LumeraMaskView
19
+ @implementation ObsrviqMaskView
20
20
 
21
21
  - (instancetype)init
22
22
  {
@@ -63,7 +63,7 @@
63
63
  // registering → honor the `masked` prop (YES = mask, NO = force-unmask).
64
64
  // !registering (leaving the tree) → clear the entry by unmasking.
65
65
  BOOL value = registering ? self.masked : NO;
66
- [LumeraReplay.shared setViewMasked:tag masked:@(value)];
66
+ [ObsrviqReplay.shared setViewMasked:tag masked:@(value)];
67
67
  }
68
68
 
69
69
  - (void)dealloc
@@ -72,7 +72,7 @@
72
72
  // without a willMoveToSuperview:nil (shouldn't happen, but tags must not leak).
73
73
  NSNumber *tag = self.reactTag;
74
74
  if (tag != nil) {
75
- [LumeraReplay.shared setViewMasked:tag masked:@(NO)];
75
+ [ObsrviqReplay.shared setViewMasked:tag masked:@(NO)];
76
76
  }
77
77
  }
78
78
 
@@ -1,21 +1,21 @@
1
1
  #import <React/RCTViewManager.h>
2
- #import "LumeraMaskView.h"
2
+ #import "ObsrviqMaskView.h"
3
3
 
4
4
  /**
5
- * View manager for the <LumeraMask> RN component (registered as native component
6
- * "LumeraMaskView"). It exposes a single `masked` prop and returns a LumeraMaskView,
5
+ * View manager for the <ObsrviqMask> RN component (registered as native component
6
+ * "ObsrviqMaskView"). It exposes a single `masked` prop and returns a ObsrviqMaskView,
7
7
  * which (un)registers its reactTag with the capture engine on mount/unmount.
8
8
  *
9
9
  * The package's codegenConfig is `type: "modules"` (TurboModule only — no Fabric
10
10
  * component spec), so this legacy RCTViewManager is the right minimal surface: RN's
11
11
  * view interop layer bridges it under both the old and new architectures.
12
12
  */
13
- @interface LumeraMaskViewManager : RCTViewManager
13
+ @interface ObsrviqMaskViewManager : RCTViewManager
14
14
  @end
15
15
 
16
- @implementation LumeraMaskViewManager
16
+ @implementation ObsrviqMaskViewManager
17
17
 
18
- RCT_EXPORT_MODULE(LumeraMaskView)
18
+ RCT_EXPORT_MODULE(ObsrviqMaskView)
19
19
 
20
20
  RCT_EXPORT_VIEW_PROPERTY(masked, BOOL)
21
21
 
@@ -23,7 +23,7 @@ RCT_EXPORT_VIEW_PROPERTY(masked, BOOL)
23
23
 
24
24
  - (UIView *)view
25
25
  {
26
- return [LumeraMaskView new];
26
+ return [ObsrviqMaskView new];
27
27
  }
28
28
 
29
29
  @end
@@ -3,7 +3,7 @@ import UIKit
3
3
  import UIKit.UIGestureRecognizerSubclass // touchesBegan/Moved/Ended overrides
4
4
 
5
5
  /**
6
- * Lumera native screenshot-replay capture engine (iOS).
6
+ * Obsrviq native screenshot-replay capture engine (iOS).
7
7
  *
8
8
  * THREADING CONTRACT (the whole point):
9
9
  * • UI thread → TWO things only: the `view.drawHierarchy(in:afterScreenUpdates:false)`
@@ -17,11 +17,11 @@ import UIKit.UIGestureRecognizerSubclass // touchesBegan/Moved/Ended overrides
17
17
  * (independent of the JS structured-event stream's seq; the worker keys frames by
18
18
  * event id, not seq, so the two streams never collide).
19
19
  *
20
- * Reached from the ObjC++ TurboModule bridge (LumeraReplayModule.mm) via the `@objc`
20
+ * Reached from the ObjC++ TurboModule bridge (ObsrviqReplayModule.mm) via the `@objc`
21
21
  * wrappers at the bottom of this file.
22
22
  */
23
- @objc(LumeraReplay)
24
- final class LumeraReplay: NSObject {
23
+ @objc(ObsrviqReplay)
24
+ final class ObsrviqReplay: NSObject {
25
25
 
26
26
  struct Options {
27
27
  var endpoint: String
@@ -39,10 +39,10 @@ final class LumeraReplay: NSObject {
39
39
 
40
40
  /// One process-wide instance — the bridge forwards every call here so the JS
41
41
  /// surface (start/stop/configure/mask/diagnostics) talks to a single engine.
42
- @objc static let shared = LumeraReplay()
42
+ @objc static let shared = ObsrviqReplay()
43
43
 
44
44
  // Serial background queue: ALL post-capture work (mask/encode/JSON/upload) lands here.
45
- private let work = DispatchQueue(label: "app.lumera.replay.work", qos: .utility)
45
+ private let work = DispatchQueue(label: "app.obsrviq.replay.work", qos: .utility)
46
46
  // URLSession on the background queue — uploads never touch the JS bridge or main thread.
47
47
  private let session: URLSession = {
48
48
  let cfg = URLSessionConfiguration.default
@@ -102,7 +102,7 @@ final class LumeraReplay: NSObject {
102
102
  // Passive touch observer: a non-consuming gesture recognizer on the key window
103
103
  // records taps + swipes without interfering with the app's own gestures.
104
104
  if opts.captureTouches, self.touchRecognizer == nil, let window = self.keyWindow() {
105
- let r = LumeraTouchRecognizer(target: self, action: #selector(self.touchNoop))
105
+ let r = ObsrviqTouchRecognizer(target: self, action: #selector(self.touchNoop))
106
106
  r.sink = self
107
107
  window.addGestureRecognizer(r)
108
108
  self.touchRecognizer = r
@@ -485,7 +485,7 @@ final class LumeraReplay: NSObject {
485
485
 
486
486
  var req = URLRequest(url: url)
487
487
  req.httpMethod = "POST"
488
- req.setValue(opts.siteKey, forHTTPHeaderField: "x-lumera-key")
488
+ req.setValue(opts.siteKey, forHTTPHeaderField: "x-obsrviq-key")
489
489
  // octet-stream, NOT application/json — the ingest reads the raw body buffer and
490
490
  // sniffs gzip itself; application/json hits Fastify's JSON parser → 400 empty_body.
491
491
  req.setValue("application/octet-stream", forHTTPHeaderField: "content-type")
@@ -615,7 +615,7 @@ final class LumeraReplay: NSObject {
615
615
  /// RFC 4122 v4 UUID for frame ids (the worker keys frame blobs by this — must be unique).
616
616
  private static func uuidv4() -> String { UUID().uuidString.lowercased() }
617
617
 
618
- // MARK: - @objc bridge surface (called from LumeraReplayModule.mm)
618
+ // MARK: - @objc bridge surface (called from ObsrviqReplayModule.mm)
619
619
  //
620
620
  // The ObjC++ TurboModule passes NSDictionary option maps straight through; these
621
621
  // wrappers unbox them into the Swift `Options` struct and forward to the real methods.
@@ -671,8 +671,8 @@ final class LumeraReplay: NSObject {
671
671
  /// A passive (non-consuming) touch observer added to the key window. Records taps +
672
672
  /// swipe samples and forwards them to the engine without interfering with the app's
673
673
  /// own gesture handling (cancelsTouchesInView = false; it never claims a gesture).
674
- final class LumeraTouchRecognizer: UIGestureRecognizer {
675
- weak var sink: LumeraReplay?
674
+ final class ObsrviqTouchRecognizer: UIGestureRecognizer {
675
+ weak var sink: ObsrviqReplay?
676
676
 
677
677
  override init(target: Any?, action: Selector?) {
678
678
  super.init(target: target, action: action)
@@ -0,0 +1,20 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <React/RCTBridgeModule.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ /**
7
+ * The native screenshot-capture TurboModule, exposed to JS as "ObsrviqReplay".
8
+ *
9
+ * This thin ObjC++ shell exists only to satisfy React Native's codegen'd New-Arch
10
+ * protocol (NativeObsrviqReplaySpec, generated from src/spec/NativeObsrviqReplay.ts via
11
+ * the `ObsrviqReplaySpec` codegen target) and to forward each call to the shared Swift
12
+ * engine `ObsrviqReplay`, which owns the entire capture → mask → encode → upload loop.
13
+ *
14
+ * No capture logic lives here. The implementation (.mm) conforms to the generated
15
+ * <NativeObsrviqReplaySpec> protocol and implements `getTurboModule:` for the New Arch.
16
+ */
17
+ @interface ObsrviqReplayModule : NSObject <RCTBridgeModule>
18
+ @end
19
+
20
+ NS_ASSUME_NONNULL_END
@@ -1,23 +1,23 @@
1
- #import "LumeraReplayModule.h"
1
+ #import "ObsrviqReplayModule.h"
2
2
 
3
- // The Swift engine (LumeraReplay, @objc-exposed) is surfaced to ObjC++ through the
3
+ // The Swift engine (ObsrviqReplay, @objc-exposed) is surfaced to ObjC++ through the
4
4
  // pod's auto-generated Swift umbrella header. CocoaPods names it
5
- // "<module_name>-Swift.h"; for this pod (`lumera-react-native`) Clang sanitizes that
6
- // to `lumera_react_native-Swift.h`. We tolerate either spelling so the bridge builds
5
+ // "<module_name>-Swift.h"; for this pod (`obsrviq-react-native`) Clang sanitizes that
6
+ // to `obsrviq_react_native-Swift.h`. We tolerate either spelling so the bridge builds
7
7
  // whether the host app links us as a pod (umbrella under the pod name) or compiles the
8
8
  // sources into the app target (umbrella under the app's product module name).
9
- #if __has_include(<lumera_react_native/lumera_react_native-Swift.h>)
10
- #import <lumera_react_native/lumera_react_native-Swift.h>
11
- #elif __has_include("lumera_react_native-Swift.h")
12
- #import "lumera_react_native-Swift.h"
13
- #elif __has_include("LumeraReactNative-Swift.h")
14
- #import "LumeraReactNative-Swift.h"
9
+ #if __has_include(<obsrviq_react_native/obsrviq_react_native-Swift.h>)
10
+ #import <obsrviq_react_native/obsrviq_react_native-Swift.h>
11
+ #elif __has_include("obsrviq_react_native-Swift.h")
12
+ #import "obsrviq_react_native-Swift.h"
13
+ #elif __has_include("ObsrviqReactNative-Swift.h")
14
+ #import "ObsrviqReactNative-Swift.h"
15
15
  #else
16
16
  // Fallback: forward-declare the @objc surface we call so the bridge still compiles if
17
17
  // the umbrella header name differs in a given app. The linker resolves it from the
18
18
  // Swift object at link time.
19
- @interface LumeraReplay : NSObject
20
- @property (class, nonatomic, readonly, strong) LumeraReplay *shared;
19
+ @interface ObsrviqReplay : NSObject
20
+ @property (class, nonatomic, readonly, strong) ObsrviqReplay *shared;
21
21
  - (void)startWithOptions:(NSDictionary *)options;
22
22
  - (void)stopObjc;
23
23
  - (void)configureWithOptions:(NSDictionary *)options;
@@ -26,19 +26,19 @@
26
26
  @end
27
27
  #endif
28
28
 
29
- // The codegen'd New-Arch spec header (target name `LumeraReplaySpec`, from
29
+ // The codegen'd New-Arch spec header (target name `ObsrviqReplaySpec`, from
30
30
  // package.json → codegenConfig.name). Present only when codegen has run (i.e. inside a
31
31
  // real app build with the New Architecture). Guarded so this file also compiles on the
32
32
  // old architecture, where the RCT_EXPORT_METHOD declarations below carry the module.
33
33
  #ifdef RCT_NEW_ARCH_ENABLED
34
- #import <LumeraReplaySpec/LumeraReplaySpec.h>
34
+ #import <ObsrviqReplaySpec/ObsrviqReplaySpec.h>
35
35
  #endif
36
36
 
37
- @implementation LumeraReplayModule
37
+ @implementation ObsrviqReplayModule
38
38
 
39
- // Registers this as the TurboModule/NativeModule named "LumeraReplay" — the exact name
40
- // TurboModuleRegistry.getEnforcing<Spec>('LumeraReplay') resolves on the JS side.
41
- RCT_EXPORT_MODULE(LumeraReplay)
39
+ // Registers this as the TurboModule/NativeModule named "ObsrviqReplay" — the exact name
40
+ // TurboModuleRegistry.getEnforcing<Spec>('ObsrviqReplay') resolves on the JS side.
41
+ RCT_EXPORT_MODULE(ObsrviqReplay)
42
42
 
43
43
  // All forwarding is trivial (dispatch onto the engine, which owns its own threads); no
44
44
  // reason to hop onto a method queue.
@@ -49,44 +49,44 @@ RCT_EXPORT_MODULE(LumeraReplay)
49
49
  // `start(options)` — object arg arrives as NSDictionary at the ObjC boundary.
50
50
  RCT_EXPORT_METHOD(start:(NSDictionary *)options)
51
51
  {
52
- [LumeraReplay.shared startWithOptions:options];
52
+ [ObsrviqReplay.shared startWithOptions:options];
53
53
  }
54
54
 
55
55
  // `stop()`
56
56
  RCT_EXPORT_METHOD(stop)
57
57
  {
58
- [LumeraReplay.shared stopObjc];
58
+ [ObsrviqReplay.shared stopObjc];
59
59
  }
60
60
 
61
61
  // `configure(options)`
62
62
  RCT_EXPORT_METHOD(configure:(NSDictionary *)options)
63
63
  {
64
- [LumeraReplay.shared configureWithOptions:options];
64
+ [ObsrviqReplay.shared configureWithOptions:options];
65
65
  }
66
66
 
67
67
  // `setViewMasked(reactTag, masked)` — numbers cross as `double`/`BOOL` on New Arch.
68
68
  RCT_EXPORT_METHOD(setViewMasked:(double)reactTag masked:(BOOL)masked)
69
69
  {
70
- [LumeraReplay.shared setViewMasked:@(reactTag) masked:@(masked)];
70
+ [ObsrviqReplay.shared setViewMasked:@(reactTag) masked:@(masked)];
71
71
  }
72
72
 
73
73
  // `getDiagnostics(): Promise<NativeDiagnostics>` — resolves with the counters snapshot.
74
74
  RCT_EXPORT_METHOD(getDiagnostics:(RCTPromiseResolveBlock)resolve
75
75
  reject:(RCTPromiseRejectBlock)reject)
76
76
  {
77
- resolve([LumeraReplay.shared getDiagnosticsObjc]);
77
+ resolve([ObsrviqReplay.shared getDiagnosticsObjc]);
78
78
  }
79
79
 
80
80
  #pragma mark - New Architecture (TurboModule) wiring
81
81
 
82
82
  #ifdef RCT_NEW_ARCH_ENABLED
83
- // Hands the runtime the C++ JSI binding generated for our spec. `NativeLumeraReplaySpecJSI`
84
- // is emitted by codegen from src/spec/NativeLumeraReplay.ts (spec target `LumeraReplaySpec`)
83
+ // Hands the runtime the C++ JSI binding generated for our spec. `NativeObsrviqReplaySpecJSI`
84
+ // is emitted by codegen from src/spec/NativeObsrviqReplay.ts (spec target `ObsrviqReplaySpec`)
85
85
  // and dispatches each JSI call to the RCT_EXPORT_METHOD implementations above.
86
86
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
87
87
  (const facebook::react::ObjCTurboModule::InitParams &)params
88
88
  {
89
- return std::make_shared<facebook::react::NativeLumeraReplaySpecJSI>(params);
89
+ return std::make_shared<facebook::react::NativeObsrviqReplaySpecJSI>(params);
90
90
  }
91
91
  #endif
92
92
 
@@ -3,17 +3,17 @@ require "json"
3
3
  package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
4
 
5
5
  Pod::Spec.new do |s|
6
- s.name = "lumera-react-native"
6
+ s.name = "obsrviq-react-native"
7
7
  s.version = package["version"]
8
8
  s.summary = package["description"]
9
- s.homepage = "https://lumera.app"
9
+ s.homepage = "https://obsrviq.com"
10
10
  s.license = "MIT"
11
- s.authors = "Lumera"
11
+ s.authors = "Obsrviq"
12
12
  s.platforms = { :ios => "13.4" }
13
- s.source = { :git => "https://github.com/TedoraTech/lumera.git", :tag => "#{s.version}" }
13
+ s.source = { :git => "https://github.com/TedoraTech/obsrviq.git", :tag => "#{s.version}" }
14
14
 
15
15
  s.source_files = "ios/**/*.{h,m,mm,swift}"
16
16
 
17
- # Pulls in React-Core, and (on New Arch) the codegen'd LumeraReplaySpec + RN-Core deps.
17
+ # Pulls in React-Core, and (on New Arch) the codegen'd ObsrviqReplaySpec + RN-Core deps.
18
18
  install_modules_dependencies(s)
19
19
  end
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@obsrviq/react-native",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "Lumera mobile capture SDK for React Native — privacy-first screenshot session replay + network/error/custom-event recording, engineered for zero added latency on device.",
7
+ "description": "Obsrviq mobile capture SDK for React Native — privacy-first screenshot session replay + network/error/custom-event recording, engineered for zero added latency on device.",
8
8
  "main": "src/index.ts",
9
9
  "module": "src/index.ts",
10
10
  "react-native": "src/index.ts",
@@ -14,7 +14,7 @@
14
14
  "src",
15
15
  "ios",
16
16
  "android",
17
- "lumera-react-native.podspec",
17
+ "obsrviq-react-native.podspec",
18
18
  "!**/__tests__"
19
19
  ],
20
20
  "dependencies": {
@@ -33,11 +33,11 @@
33
33
  "typescript": "^5.7.2"
34
34
  },
35
35
  "codegenConfig": {
36
- "name": "LumeraReplaySpec",
36
+ "name": "ObsrviqReplaySpec",
37
37
  "type": "modules",
38
38
  "jsSrcsDir": "src/spec",
39
39
  "android": {
40
- "javaPackageName": "app.lumera.replay"
40
+ "javaPackageName": "app.obsrviq.replay"
41
41
  }
42
42
  },
43
43
  "scripts": {
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { Platform, View, type ViewProps, requireNativeComponent } from 'react-native';
3
+
4
+ /**
5
+ * <ObsrviqMask> — privacy-by-region wrapper for the native screenshot replay.
6
+ *
7
+ * Wrap any subtree to control whether it is redacted in captured frames:
8
+ *
9
+ * <ObsrviqMask> …</ObsrviqMask> // force-mask this subtree
10
+ * <ObsrviqMask unmask> …</ObsrviqMask> // force-UNMASK (overrides maskAllText/Images)
11
+ *
12
+ * Under the hood the native view (un)registers its reactTag with the capture engine via
13
+ * `setViewMasked(reactTag, …)`. Masking is composited natively, off the UI thread, during
14
+ * capture — this component renders its children verbatim and adds no per-frame cost.
15
+ *
16
+ * On unsupported platforms / unlinked builds (e.g. Expo Go) it degrades to a plain <View>
17
+ * so layout is unaffected. (`<LumeraMask>` remains exported as a deprecated alias.)
18
+ */
19
+ export interface ObsrviqMaskProps extends ViewProps {
20
+ /** Force-unmask this subtree (overrides the global maskAllText/maskAllImages defaults). */
21
+ unmask?: boolean;
22
+ children?: React.ReactNode;
23
+ }
24
+
25
+ interface NativeMaskProps extends ViewProps {
26
+ masked: boolean;
27
+ children?: React.ReactNode;
28
+ }
29
+
30
+ // Resolve the native component lazily + defensively. Prefer the Obsrviq-era view, fall
31
+ // back to the legacy "LumeraMaskView" name (pre-rebrand native binaries); if neither is
32
+ // linked the require throws and we degrade to a plain View (capture just won't mask here).
33
+ const NativeObsrviqMask: React.ComponentType<NativeMaskProps> | null = (() => {
34
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') return null;
35
+ // Try the Obsrviq view, then legacy names: "LumeraMaskView" (iOS pre-rebrand) and
36
+ // "LumeraMask" (Android pre-rebrand — they disagreed before the rename).
37
+ for (const name of ['ObsrviqMaskView', 'LumeraMaskView', 'LumeraMask']) {
38
+ try {
39
+ return requireNativeComponent<NativeMaskProps>(name);
40
+ } catch {
41
+ /* try the next name */
42
+ }
43
+ }
44
+ return null;
45
+ })();
46
+
47
+ export function ObsrviqMask({ unmask, children, ...rest }: ObsrviqMaskProps): React.ReactElement {
48
+ if (!NativeObsrviqMask) {
49
+ return <View {...rest}>{children}</View>;
50
+ }
51
+ return (
52
+ <NativeObsrviqMask masked={!unmask} {...rest}>
53
+ {children}
54
+ </NativeObsrviqMask>
55
+ );
56
+ }
57
+
58
+ /** @deprecated Renamed to {@link ObsrviqMask}. Kept as an alias for the Lumera era. */
59
+ export const LumeraMask = ObsrviqMask;
60
+ /** @deprecated Renamed to {@link ObsrviqMaskProps}. */
61
+ export type LumeraMaskProps = ObsrviqMaskProps;
62
+
63
+ export default ObsrviqMask;
package/src/config.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import type { IngestBatch } from '@obsrviq/types';
2
2
 
3
3
  /**
4
- * Public init config for the Lumera React Native SDK. Mirrors the web tracker's
4
+ * Public init config for the Obsrviq React Native SDK. Mirrors the web tracker's
5
5
  * `ObsrviqInitConfig` where it makes sense, swapping DOM-specific privacy knobs for
6
6
  * the native screenshot-replay knobs (fps / jpegQuality / maskAll*).
7
7
  */
8
- export interface LumeraConfig {
8
+ export interface ObsrviqConfig {
9
9
  /** REQUIRED — your ingest key (pk_…). */
10
10
  siteKey: string;
11
11
 
@@ -66,9 +66,9 @@ export interface LumeraConfig {
66
66
  }
67
67
 
68
68
  export type ResolvedConfig = Required<
69
- Omit<LumeraConfig, 'userId' | 'metadata' | 'beforeSend'>
69
+ Omit<ObsrviqConfig, 'userId' | 'metadata' | 'beforeSend'>
70
70
  > &
71
- Pick<LumeraConfig, 'userId' | 'metadata' | 'beforeSend'>;
71
+ Pick<ObsrviqConfig, 'userId' | 'metadata' | 'beforeSend'>;
72
72
 
73
73
  export const DEFAULTS: Omit<ResolvedConfig, 'siteKey' | 'userId' | 'metadata' | 'beforeSend'> = {
74
74
  enableReplay: true,
@@ -89,8 +89,11 @@ export const DEFAULTS: Omit<ResolvedConfig, 'siteKey' | 'userId' | 'metadata' |
89
89
  requireConsent: false,
90
90
  };
91
91
 
92
- export function resolveConfig(config: LumeraConfig): ResolvedConfig {
92
+ export function resolveConfig(config: ObsrviqConfig): ResolvedConfig {
93
93
  const clean: Record<string, unknown> = {};
94
94
  for (const [k, v] of Object.entries(config)) if (v !== undefined) clean[k] = v;
95
95
  return { ...DEFAULTS, ...(clean as Partial<ResolvedConfig>), siteKey: config.siteKey };
96
96
  }
97
+
98
+ /** @deprecated Renamed to {@link ObsrviqConfig}. Kept as an alias for the Lumera era. */
99
+ export type LumeraConfig = ObsrviqConfig;
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Dimensions, Platform } from 'react-native';
2
2
  import type { IngestBatch, IngestSessionMeta, ObsrviqEvent } from '@obsrviq/types';
3
- import NativeLumeraReplay from './spec/NativeLumeraReplay';
4
- import { type LumeraConfig, type ResolvedConfig, resolveConfig } from './config';
3
+ import NativeObsrviqReplay from './spec/NativeObsrviqReplay';
4
+ import { type ObsrviqConfig, type ResolvedConfig, resolveConfig } from './config';
5
5
  import { Clock, uuid } from './util';
6
6
  import { clearSession, persistProgress, resolveSession, type ResolvedSession } from './session';
7
7
  import { Transport } from './transport';
@@ -19,13 +19,13 @@ export interface Diagnostics {
19
19
  }
20
20
 
21
21
  /**
22
- * Lumera React Native capture SDK. Mirrors the web tracker's public API 1:1, so app
22
+ * Obsrviq React Native capture SDK. Mirrors the web tracker's public API 1:1, so app
23
23
  * code reads the same on web and mobile. The pure-JS side here owns identity, the
24
24
  * structured-event stream (network / error / console / custom), and transport; the
25
25
  * heavy screenshot replay stream is owned end-to-end by the native TurboModule
26
26
  * (capture → mask → encode → upload), which never blocks the JS thread.
27
27
  */
28
- class LumeraReplayClient {
28
+ class ObsrviqReplayClient {
29
29
  private cfg: ResolvedConfig | null = null;
30
30
  private clock: Clock | null = null;
31
31
  private transport: Transport | null = null;
@@ -47,7 +47,7 @@ class LumeraReplayClient {
47
47
 
48
48
  /** Initialize the SDK. Idempotent. Session resolution is async (AsyncStorage); any
49
49
  * events emitted before it resolves are buffered and timestamped correctly. */
50
- init(config: LumeraConfig): void {
50
+ init(config: ObsrviqConfig): void {
51
51
  if (this.cfg) return;
52
52
  const cfg = resolveConfig(config);
53
53
  // Sampling: decide once, up front. A sampled-out session records nothing.
@@ -121,7 +121,7 @@ class LumeraReplayClient {
121
121
  // ── Native screenshot capture (owns capture + encode + upload, off the UI/JS thread) ──
122
122
  if (cfg.enableReplay) {
123
123
  try {
124
- NativeLumeraReplay.start({
124
+ NativeObsrviqReplay.start({
125
125
  endpoint: cfg.endpoint,
126
126
  siteKey: cfg.siteKey,
127
127
  sessionId: this._sessionId!,
@@ -237,7 +237,7 @@ class LumeraReplayClient {
237
237
  for (const u of this.uninstalls.splice(0)) u();
238
238
  if (this.cfg?.enableReplay) {
239
239
  try {
240
- NativeLumeraReplay.stop();
240
+ NativeObsrviqReplay.stop();
241
241
  } catch {
242
242
  /* not linked */
243
243
  }
@@ -284,9 +284,15 @@ class LumeraReplayClient {
284
284
  }
285
285
  }
286
286
 
287
- export const LumeraReplay = new LumeraReplayClient();
288
- export default LumeraReplay;
289
- export type { LumeraConfig };
287
+ export const ObsrviqReplay = new ObsrviqReplayClient();
288
+ export default ObsrviqReplay;
289
+ export type { ObsrviqConfig };
290
290
 
291
- // Privacy-by-region component for the native screenshot replay (<LumeraMask> / <LumeraMask unmask>).
292
- export { LumeraMask, type LumeraMaskProps } from './LumeraMask';
291
+ /** @deprecated Renamed to {@link ObsrviqReplay}. Kept as an alias for the Lumera era. */
292
+ export const LumeraReplay = ObsrviqReplay;
293
+ /** @deprecated Renamed to {@link ObsrviqConfig}. */
294
+ export type { ObsrviqConfig as LumeraConfig };
295
+
296
+ // Privacy-by-region component for the native screenshot replay (<ObsrviqMask> / <ObsrviqMask unmask>).
297
+ // `LumeraMask` is re-exported as a deprecated alias.
298
+ export { ObsrviqMask, LumeraMask, type ObsrviqMaskProps, type LumeraMaskProps } from './ObsrviqMask';
package/src/session.ts CHANGED
@@ -1,8 +1,17 @@
1
1
  import AsyncStorage from '@react-native-async-storage/async-storage';
2
2
  import { uuid } from './util';
3
3
 
4
- const SESSION_KEY = 'lumera.session';
5
- const VISITOR_KEY = 'lumera.visitor';
4
+ const SESSION_KEY = 'obsrviq.session';
5
+ const VISITOR_KEY = 'obsrviq.visitor';
6
+ // Pre-rebrand keys — still read so an upgrading app keeps its visitor identity + any
7
+ // in-flight session continuation; we migrate onto the new keys on the next write.
8
+ const LEGACY_SESSION_KEY = 'lumera.session';
9
+ const LEGACY_VISITOR_KEY = 'lumera.visitor';
10
+
11
+ /** Read the current key, falling back to the legacy Lumera key for upgrading apps. */
12
+ async function getStored(key: string, legacyKey: string): Promise<string | null> {
13
+ return (await AsyncStorage.getItem(key)) ?? (await AsyncStorage.getItem(legacyKey));
14
+ }
6
15
 
7
16
  interface StoredSession {
8
17
  id: string;
@@ -54,7 +63,7 @@ export async function resolveSession(opts: {
54
63
  const now = opts.now ?? Date.now();
55
64
 
56
65
  // ── Visitor (persistent, anonymous) ──
57
- let visitor = safeParse<StoredVisitor>(await AsyncStorage.getItem(VISITOR_KEY));
66
+ let visitor = safeParse<StoredVisitor>(await getStored(VISITOR_KEY, LEGACY_VISITOR_KEY));
58
67
  if (!visitor) {
59
68
  visitor = { id: uuid(), firstSeenAt: now, visitCount: 0 };
60
69
  }
@@ -62,7 +71,7 @@ export async function resolveSession(opts: {
62
71
  // ── Session (optional continuation) ──
63
72
  let session: StoredSession | null = null;
64
73
  if (opts.continueSession) {
65
- const prior = safeParse<StoredSession>(await AsyncStorage.getItem(SESSION_KEY));
74
+ const prior = safeParse<StoredSession>(await getStored(SESSION_KEY, LEGACY_SESSION_KEY));
66
75
  const fresh = prior && now - prior.lastActivity < opts.timeoutMs;
67
76
  const sameUser = !opts.userId || !prior?.userId || prior.userId === opts.userId;
68
77
  if (prior && fresh && sameUser) session = prior;
@@ -80,6 +89,8 @@ export async function resolveSession(opts: {
80
89
  [SESSION_KEY, JSON.stringify(session)],
81
90
  [VISITOR_KEY, JSON.stringify(visitor)],
82
91
  ]);
92
+ // Migrate off the legacy keys so they don't go stale alongside the new ones.
93
+ await AsyncStorage.multiRemove([LEGACY_SESSION_KEY, LEGACY_VISITOR_KEY]);
83
94
 
84
95
  return {
85
96
  id: session.id,
@@ -94,7 +105,7 @@ export async function resolveSession(opts: {
94
105
 
95
106
  /** Persist the latest seq + activity so a continued launch resumes correctly. */
96
107
  export async function persistProgress(id: string, seq: number, userId?: string): Promise<void> {
97
- const prior = safeParse<StoredSession>(await AsyncStorage.getItem(SESSION_KEY));
108
+ const prior = safeParse<StoredSession>(await getStored(SESSION_KEY, LEGACY_SESSION_KEY));
98
109
  if (!prior || prior.id !== id) return; // a reset() already moved on — don't clobber
99
110
  await AsyncStorage.setItem(
100
111
  SESSION_KEY,
@@ -104,5 +115,5 @@ export async function persistProgress(id: string, seq: number, userId?: string):
104
115
 
105
116
  /** reset() / logout: drop the continuation record so the next session can't resume this user. */
106
117
  export async function clearSession(): Promise<void> {
107
- await AsyncStorage.removeItem(SESSION_KEY);
118
+ await AsyncStorage.multiRemove([SESSION_KEY, LEGACY_SESSION_KEY]);
108
119
  }
@@ -61,10 +61,14 @@ export interface Spec extends TurboModule {
61
61
  stop(): void;
62
62
  /** Live-tune cadence/quality/masking without restarting. */
63
63
  configure(options: ConfigureOptions): void;
64
- /** Explicitly mask/unmask a native view by its RN reactTag (powers <LumeraMask>). */
64
+ /** Explicitly mask/unmask a native view by its RN reactTag (powers <ObsrviqMask>). */
65
65
  setViewMasked(reactTag: number, masked: boolean): void;
66
66
  /** Native-side counters for diagnostics (cheap; async so it never blocks). */
67
67
  getDiagnostics(): Promise<NativeDiagnostics>;
68
68
  }
69
69
 
70
- export default TurboModuleRegistry.getEnforcing<Spec>('LumeraReplay');
70
+ // Prefer the Obsrviq-era native module; fall back to the legacy "LumeraReplay" name so an
71
+ // app still running a pre-rebrand native binary keeps working until it rebuilds. `get`
72
+ // (not `getEnforcing`) returns null when neither is linked — callers guard with try/catch.
73
+ export default (TurboModuleRegistry.get<Spec>('ObsrviqReplay') ??
74
+ TurboModuleRegistry.get<Spec>('LumeraReplay')) as Spec;
package/src/transport.ts CHANGED
@@ -28,7 +28,8 @@ export class Transport {
28
28
  // `application/json` would hit Fastify's built-in JSON parser, leaving the
29
29
  // route's Buffer.isBuffer() check false → 400 "empty_body". (Matches the web tracker.)
30
30
  'content-type': 'application/octet-stream',
31
- 'x-lumera-key': this.siteKey,
31
+ // Ingest accepts x-obsrviq-key (primary) and x-lumera-key (legacy). Send the new one.
32
+ 'x-obsrviq-key': this.siteKey,
32
33
  },
33
34
  body: JSON.stringify(batch),
34
35
  });
@@ -1,39 +0,0 @@
1
- package app.lumera.replay
2
-
3
- import android.view.View
4
- import com.facebook.react.module.annotations.ReactModule
5
- import com.facebook.react.uimanager.SimpleViewManager
6
- import com.facebook.react.uimanager.ThemedReactContext
7
-
8
- /**
9
- * Backs the optional `<LumeraMask>` JS component. It is a plain pass-through container
10
- * (children render normally); its sole job is to tell the capture engine "redact my
11
- * subtree". On attach it registers its own View.id in [LumeraReplayModule.maskedTags];
12
- * on detach it removes it. This is the geometry [LumeraReplayModule.collectMaskRects]
13
- * walks: an explicitly-masked container yields one redaction block over the whole subtree.
14
- *
15
- * Kept deliberately minimal — no custom props, no shadow node. JS can also drive masking
16
- * imperatively via `NativeLumeraReplay.setViewMasked(reactTag, masked)` without this view.
17
- */
18
- @ReactModule(name = LumeraMaskViewManager.REACT_CLASS)
19
- class LumeraMaskViewManager : SimpleViewManager<View>() {
20
-
21
- override fun getName() = REACT_CLASS
22
-
23
- override fun createViewInstance(reactContext: ThemedReactContext): View =
24
- object : View(reactContext) {
25
- override fun onAttachedToWindow() {
26
- super.onAttachedToWindow()
27
- LumeraReplayModule.maskTag(id)
28
- }
29
-
30
- override fun onDetachedFromWindow() {
31
- LumeraReplayModule.unmaskTag(id)
32
- super.onDetachedFromWindow()
33
- }
34
- }
35
-
36
- companion object {
37
- const val REACT_CLASS = "LumeraMask"
38
- }
39
- }
@@ -1,20 +0,0 @@
1
- #import <Foundation/Foundation.h>
2
- #import <React/RCTBridgeModule.h>
3
-
4
- NS_ASSUME_NONNULL_BEGIN
5
-
6
- /**
7
- * The native screenshot-capture TurboModule, exposed to JS as "LumeraReplay".
8
- *
9
- * This thin ObjC++ shell exists only to satisfy React Native's codegen'd New-Arch
10
- * protocol (NativeLumeraReplaySpec, generated from src/spec/NativeLumeraReplay.ts via
11
- * the `LumeraReplaySpec` codegen target) and to forward each call to the shared Swift
12
- * engine `LumeraReplay`, which owns the entire capture → mask → encode → upload loop.
13
- *
14
- * No capture logic lives here. The implementation (.mm) conforms to the generated
15
- * <NativeLumeraReplaySpec> protocol and implements `getTurboModule:` for the New Arch.
16
- */
17
- @interface LumeraReplayModule : NSObject <RCTBridgeModule>
18
- @end
19
-
20
- NS_ASSUME_NONNULL_END
@@ -1,52 +0,0 @@
1
- import React from 'react';
2
- import { Platform, View, type ViewProps, requireNativeComponent } from 'react-native';
3
-
4
- /**
5
- * <LumeraMask> — privacy-by-region wrapper for the native screenshot replay.
6
- *
7
- * Wrap any subtree to control whether it is redacted in captured frames:
8
- *
9
- * <LumeraMask> …</LumeraMask> // force-mask this subtree
10
- * <LumeraMask unmask> …</LumeraMask> // force-UNMASK (overrides maskAllText/Images)
11
- *
12
- * Under the hood the native view (un)registers its reactTag with the capture engine via
13
- * `setViewMasked(reactTag, …)`. Masking is composited natively, off the UI thread, during
14
- * capture — this component renders its children verbatim and adds no per-frame cost.
15
- *
16
- * The native view exists on iOS (and Android, task #96). On unsupported platforms /
17
- * unlinked builds (e.g. Expo Go) it degrades to a plain <View> so layout is unaffected.
18
- */
19
- export interface LumeraMaskProps extends ViewProps {
20
- /** Force-unmask this subtree (overrides the global maskAllText/maskAllImages defaults). */
21
- unmask?: boolean;
22
- children?: React.ReactNode;
23
- }
24
-
25
- interface NativeMaskProps extends ViewProps {
26
- masked: boolean;
27
- children?: React.ReactNode;
28
- }
29
-
30
- // Resolve the native component lazily + defensively: if the native module isn't linked
31
- // the require throws, and we fall back to a plain View (capture simply won't mask here).
32
- const NativeLumeraMask: React.ComponentType<NativeMaskProps> | null = (() => {
33
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') return null;
34
- try {
35
- return requireNativeComponent<NativeMaskProps>('LumeraMaskView');
36
- } catch {
37
- return null;
38
- }
39
- })();
40
-
41
- export function LumeraMask({ unmask, children, ...rest }: LumeraMaskProps): React.ReactElement {
42
- if (!NativeLumeraMask) {
43
- return <View {...rest}>{children}</View>;
44
- }
45
- return (
46
- <NativeLumeraMask masked={!unmask} {...rest}>
47
- {children}
48
- </NativeLumeraMask>
49
- );
50
- }
51
-
52
- export default LumeraMask;