@repliqo/sdk-react-native 0.3.8 → 0.4.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.
Files changed (42) hide show
  1. package/INTEGRATION_GUIDE.md +1384 -1312
  2. package/android/src/main/java/com/repliqo/screencapture/MultiWindowCapture.java +242 -242
  3. package/android/src/main/java/com/repliqo/screencapture/ScreenCaptureModule.java +94 -94
  4. package/dist/components/RepliqoFlatList.d.ts +5 -0
  5. package/dist/components/RepliqoFlatList.js +39 -0
  6. package/dist/components/RepliqoScrollView.d.ts +3 -0
  7. package/dist/components/RepliqoScrollView.js +16 -265
  8. package/dist/components/useRepliqoScrollTracking.d.ts +33 -0
  9. package/dist/components/useRepliqoScrollTracking.js +304 -0
  10. package/dist/core/client.d.ts +52 -0
  11. package/dist/core/client.js +304 -16
  12. package/dist/core/config.d.ts +1 -1
  13. package/dist/core/config.js +5 -1
  14. package/dist/core/storage.d.ts +29 -0
  15. package/dist/core/storage.js +33 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +3 -1
  18. package/dist/snapshot/capture.d.ts +7 -0
  19. package/dist/snapshot/capture.js +17 -5
  20. package/dist/trackers/error.tracker.d.ts +25 -0
  21. package/dist/trackers/error.tracker.js +114 -37
  22. package/dist/trackers/navigation.tracker.js +11 -0
  23. package/dist/transport/api.client.d.ts +34 -1
  24. package/dist/transport/api.client.js +102 -19
  25. package/dist/transport/batch-queue.d.ts +53 -1
  26. package/dist/transport/batch-queue.js +126 -19
  27. package/dist/types/events.d.ts +12 -0
  28. package/package.json +68 -64
  29. package/src/components/RepliqoFlatList.tsx +64 -0
  30. package/src/components/RepliqoScrollView.tsx +53 -302
  31. package/src/components/useRepliqoScrollTracking.ts +366 -0
  32. package/src/core/client.ts +953 -628
  33. package/src/core/config.ts +38 -30
  34. package/src/core/storage.ts +51 -0
  35. package/src/index.ts +54 -50
  36. package/src/snapshot/NativeScreenCapture.ts +62 -62
  37. package/src/snapshot/capture.ts +154 -142
  38. package/src/trackers/error.tracker.ts +211 -136
  39. package/src/trackers/navigation.tracker.ts +107 -98
  40. package/src/transport/api.client.ts +430 -327
  41. package/src/transport/batch-queue.ts +212 -95
  42. package/src/types/events.ts +72 -60
@@ -1,30 +1,38 @@
1
- import { SDKConfig } from '../types/events';
2
-
3
- export const REPLIQO_API_URL = 'https://api-repliqo.odmservice.site';
4
-
5
- export const DEFAULT_CONFIG: Required<Omit<SDKConfig, 'apiKey' | 'appId'>> = {
6
- batchSize: 20,
7
- flushInterval: 30000,
8
- enableTouchTracking: true,
9
- enableNavigationTracking: true,
10
- enableSnapshots: true,
11
- // 0.5 fps. JPEG screenshots at 390px/40% quality are ~15-30 KB each,
12
- // so 2 s gives a good quality/bandwidth tradeoff for MVP session replay.
13
- snapshotInterval: 2000,
14
- // Cap sessions at ~30 minutes of continuous capture (900 frames).
15
- maxSnapshotsPerSession: 900,
16
- // Smaller batches than events - JPEGs are heavier.
17
- snapshotBatchSize: 8,
18
- snapshotFlushInterval: 15000,
19
- // ~32 * 30KB ≈ 1 MB cap on in-memory buffer if the backend is down.
20
- snapshotMaxBufferSize: 32,
21
- enableHeatmapCapture: true,
22
- enableCrashTracking: true,
23
- debug: false,
24
- };
25
-
26
- export type ResolvedConfig = SDKConfig & typeof DEFAULT_CONFIG;
27
-
28
- export function resolveConfig(config: SDKConfig): ResolvedConfig {
29
- return { ...DEFAULT_CONFIG, ...config };
30
- }
1
+ import { SDKConfig } from '../types/events';
2
+
3
+ export const REPLIQO_API_URL = 'https://api-repliqo.odmservice.site';
4
+
5
+ export const DEFAULT_CONFIG: Required<
6
+ Omit<SDKConfig, 'apiKey' | 'appId' | 'storage'>
7
+ > = {
8
+ batchSize: 20,
9
+ flushInterval: 30000,
10
+ enableTouchTracking: true,
11
+ enableNavigationTracking: true,
12
+ enableSnapshots: true,
13
+ // 0.5 fps. JPEG screenshots at 390px/40% quality are ~15-30 KB each,
14
+ // so 2 s gives a good quality/bandwidth tradeoff for MVP session replay.
15
+ snapshotInterval: 2000,
16
+ // Cap sessions at ~30 minutes of continuous capture (900 frames).
17
+ maxSnapshotsPerSession: 900,
18
+ // Smaller batches than events - JPEGs are heavier.
19
+ snapshotBatchSize: 8,
20
+ snapshotFlushInterval: 15000,
21
+ // ~32 * 30KB ≈ 1 MB cap on in-memory buffer if the backend is down.
22
+ snapshotMaxBufferSize: 32,
23
+ enableHeatmapCapture: true,
24
+ enableCrashTracking: true,
25
+ debug: false,
26
+ };
27
+
28
+ export type ResolvedConfig = SDKConfig & typeof DEFAULT_CONFIG;
29
+
30
+ export function resolveConfig(config: SDKConfig): ResolvedConfig {
31
+ // Strip keys whose value is explicitly `undefined` — otherwise
32
+ // `{ enableSnapshots: maybeUndefinedVar }` would silently override the
33
+ // default `true` with `undefined` and disable the feature.
34
+ const cleaned = Object.fromEntries(
35
+ Object.entries(config).filter(([, v]) => v !== undefined),
36
+ ) as SDKConfig;
37
+ return { ...DEFAULT_CONFIG, ...cleaned };
38
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Minimal async key-value storage contract, compatible with
3
+ * @react-native-async-storage/async-storage (and trivially mockable).
4
+ *
5
+ * Used to persist analytics data across app kills:
6
+ * - buffered events that couldn't be flushed before backgrounding
7
+ * - crash reports (a fatal crash kills the app before the report's
8
+ * network request can complete — persistence is the only way those
9
+ * reports survive to the next launch)
10
+ */
11
+ export interface PersistentStorage {
12
+ getItem(key: string): Promise<string | null>;
13
+ setItem(key: string, value: string): Promise<void>;
14
+ removeItem(key: string): Promise<void>;
15
+ }
16
+
17
+ /* Dynamic optional require — AsyncStorage is an optional peer. */
18
+ declare const require: any;
19
+
20
+ /**
21
+ * Try to auto-load @react-native-async-storage/async-storage. Returns null
22
+ * when the host app doesn't have it installed — persistence is then
23
+ * disabled gracefully (in-memory only, pre-existing behavior).
24
+ */
25
+ export function tryLoadAsyncStorage(): PersistentStorage | null {
26
+ try {
27
+ const mod = require('@react-native-async-storage/async-storage');
28
+ const storage = mod?.default ?? mod;
29
+ if (
30
+ storage &&
31
+ typeof storage.getItem === 'function' &&
32
+ typeof storage.setItem === 'function' &&
33
+ typeof storage.removeItem === 'function'
34
+ ) {
35
+ return storage as PersistentStorage;
36
+ }
37
+ } catch {
38
+ // Not installed — fine.
39
+ }
40
+ return null;
41
+ }
42
+
43
+ /** Storage keys namespaced to avoid colliding with the host app's data. */
44
+ export const STORAGE_KEYS = {
45
+ pendingEvents: '@repliqo/pending_events',
46
+ pendingCrashes: '@repliqo/pending_crashes',
47
+ } as const;
48
+
49
+ /** Caps so a long-offline device can't grow storage unboundedly. */
50
+ export const MAX_PERSISTED_EVENTS = 500;
51
+ export const MAX_PERSISTED_CRASHES = 20;
package/src/index.ts CHANGED
@@ -1,50 +1,54 @@
1
- // Main class
2
- export { AppAnalytics } from './core/client';
3
-
4
- // Trackers
5
- export { TouchTracker } from './trackers/touch.tracker';
6
- export {
7
- useNavigationTracker,
8
- trackScreenChange,
9
- } from './trackers/navigation.tracker';
10
- export {
11
- trackScreenEnter,
12
- trackScreenExit,
13
- } from './trackers/screen.tracker';
14
- export { ErrorTracker } from './trackers/error.tracker';
15
-
16
- // Snapshot
17
- export { SnapshotCapture } from './snapshot/capture';
18
- export { captureScreenshot } from './snapshot/captureScreenshot';
19
- export {
20
- isNativeScreenCaptureAvailable,
21
- captureNativeFrame,
22
- } from './snapshot/NativeScreenCapture';
23
-
24
- // Types
25
- export type {
26
- EventType,
27
- AnalyticsEvent,
28
- TouchEventData,
29
- NavigationEventData,
30
- ScreenVisit,
31
- DeviceInfo,
32
- SDKConfig,
33
- } from './types/events';
34
-
35
- export type {
36
- ScreenshotData,
37
- SnapshotPayload,
38
- } from './types/snapshot';
39
-
40
- export type { CrashReport } from './types/crash';
41
-
42
- export type { SnapshotCaptureConfig } from './snapshot/capture';
43
- export type { ScreenshotResult } from './snapshot/captureScreenshot';
44
-
45
- // Components
46
- export { RepliqoScrollView } from './components/RepliqoScrollView';
47
-
48
- // Config
49
- export { DEFAULT_CONFIG } from './core/config';
50
- export type { ResolvedConfig } from './core/config';
1
+ // Main class
2
+ export { AppAnalytics } from './core/client';
3
+
4
+ // Trackers
5
+ export { TouchTracker } from './trackers/touch.tracker';
6
+ export {
7
+ useNavigationTracker,
8
+ trackScreenChange,
9
+ } from './trackers/navigation.tracker';
10
+ export {
11
+ trackScreenEnter,
12
+ trackScreenExit,
13
+ } from './trackers/screen.tracker';
14
+ export { ErrorTracker } from './trackers/error.tracker';
15
+
16
+ // Snapshot
17
+ export { SnapshotCapture } from './snapshot/capture';
18
+ export { captureScreenshot } from './snapshot/captureScreenshot';
19
+ export {
20
+ isNativeScreenCaptureAvailable,
21
+ captureNativeFrame,
22
+ } from './snapshot/NativeScreenCapture';
23
+
24
+ // Types
25
+ export type {
26
+ EventType,
27
+ AnalyticsEvent,
28
+ TouchEventData,
29
+ NavigationEventData,
30
+ ScreenVisit,
31
+ DeviceInfo,
32
+ SDKConfig,
33
+ } from './types/events';
34
+
35
+ export type {
36
+ ScreenshotData,
37
+ SnapshotPayload,
38
+ } from './types/snapshot';
39
+
40
+ export type { CrashReport } from './types/crash';
41
+
42
+ export type { SnapshotCaptureConfig } from './snapshot/capture';
43
+ export type { ScreenshotResult } from './snapshot/captureScreenshot';
44
+
45
+ // Components
46
+ export { RepliqoScrollView } from './components/RepliqoScrollView';
47
+ export { RepliqoFlatList } from './components/RepliqoFlatList';
48
+
49
+ // Storage (offline persistence contract)
50
+ export type { PersistentStorage } from './core/storage';
51
+
52
+ // Config
53
+ export { DEFAULT_CONFIG } from './core/config';
54
+ export type { ResolvedConfig } from './core/config';
@@ -1,62 +1,62 @@
1
- import { NativeModules, Platform } from 'react-native';
2
-
3
- export interface NativeFrameResult {
4
- image: string;
5
- width: number;
6
- height: number;
7
- format: 'jpeg';
8
- }
9
-
10
- interface NativeCaptureResult {
11
- image: string;
12
- width: number;
13
- height: number;
14
- format: string;
15
- }
16
-
17
- interface NativeScreenCaptureModule {
18
- captureFrame(): Promise<NativeCaptureResult | null>;
19
- isAvailable(): Promise<boolean>;
20
- }
21
-
22
- const NativeModule: NativeScreenCaptureModule | null =
23
- Platform.OS === 'android'
24
- ? (NativeModules.ScreenCaptureModule as NativeScreenCaptureModule) ?? null
25
- : null;
26
-
27
- /** Whether the native multi-window capture module is available. */
28
- export function isNativeScreenCaptureAvailable(): boolean {
29
- return NativeModule !== null;
30
- }
31
-
32
- /**
33
- * Capture a single frame using native multi-window capture.
34
- * Captures ALL visible windows: main view + modals + alerts + dialogs.
35
- *
36
- * No permissions required. No dialog. No foreground service.
37
- *
38
- * This is the ONLY native capture path. Full-content capture of
39
- * off-screen ScrollView content is NOT possible natively (React Native
40
- * doesn't render off-screen content to the GPU/view tree) — full-content
41
- * heatmap backgrounds are instead built collaboratively from viewport
42
- * tiles captured as the user naturally scrolls (see RepliqoScrollView).
43
- *
44
- * @returns NativeFrameResult or null if capture failed / not available
45
- */
46
- export async function captureNativeFrame(): Promise<NativeFrameResult | null> {
47
- if (!NativeModule) return null;
48
-
49
- try {
50
- const result = await NativeModule.captureFrame();
51
- if (!result || !result.image) return null;
52
-
53
- return {
54
- image: result.image,
55
- width: result.width,
56
- height: result.height,
57
- format: 'jpeg',
58
- };
59
- } catch {
60
- return null;
61
- }
62
- }
1
+ import { NativeModules, Platform } from 'react-native';
2
+
3
+ export interface NativeFrameResult {
4
+ image: string;
5
+ width: number;
6
+ height: number;
7
+ format: 'jpeg';
8
+ }
9
+
10
+ interface NativeCaptureResult {
11
+ image: string;
12
+ width: number;
13
+ height: number;
14
+ format: string;
15
+ }
16
+
17
+ interface NativeScreenCaptureModule {
18
+ captureFrame(): Promise<NativeCaptureResult | null>;
19
+ isAvailable(): Promise<boolean>;
20
+ }
21
+
22
+ const NativeModule: NativeScreenCaptureModule | null =
23
+ Platform.OS === 'android'
24
+ ? (NativeModules.ScreenCaptureModule as NativeScreenCaptureModule) ?? null
25
+ : null;
26
+
27
+ /** Whether the native multi-window capture module is available. */
28
+ export function isNativeScreenCaptureAvailable(): boolean {
29
+ return NativeModule !== null;
30
+ }
31
+
32
+ /**
33
+ * Capture a single frame using native multi-window capture.
34
+ * Captures ALL visible windows: main view + modals + alerts + dialogs.
35
+ *
36
+ * No permissions required. No dialog. No foreground service.
37
+ *
38
+ * This is the ONLY native capture path. Full-content capture of
39
+ * off-screen ScrollView content is NOT possible natively (React Native
40
+ * doesn't render off-screen content to the GPU/view tree) — full-content
41
+ * heatmap backgrounds are instead built collaboratively from viewport
42
+ * tiles captured as the user naturally scrolls (see RepliqoScrollView).
43
+ *
44
+ * @returns NativeFrameResult or null if capture failed / not available
45
+ */
46
+ export async function captureNativeFrame(): Promise<NativeFrameResult | null> {
47
+ if (!NativeModule) return null;
48
+
49
+ try {
50
+ const result = await NativeModule.captureFrame();
51
+ if (!result || !result.image) return null;
52
+
53
+ return {
54
+ image: result.image,
55
+ width: result.width,
56
+ height: result.height,
57
+ format: 'jpeg',
58
+ };
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
@@ -1,142 +1,154 @@
1
- import { SnapshotPayload, ScreenshotData } from '../types/snapshot';
2
- import { captureScreenshot } from './captureScreenshot';
3
-
4
- export interface SnapshotCaptureConfig {
5
- captureInterval: number;
6
- maxSnapshotsPerSession: number;
7
- }
8
-
9
- export type ScreenshotProvider = () => Promise<ScreenshotData | null>;
10
-
11
- type LogFn = (...args: any[]) => void;
12
-
13
- /**
14
- * Captures JPEG screenshots of the RN view hierarchy at a fixed interval
15
- * and forwards them to the host (usually the snapshot BatchQueue).
16
- */
17
- export class SnapshotCapture {
18
- private timeoutId: ReturnType<typeof setTimeout> | null = null;
19
- private sequence: number = 0;
20
- private config: SnapshotCaptureConfig;
21
- private onSnapshot: (snapshot: SnapshotPayload) => void;
22
- private sessionId: string | null = null;
23
- private currentScreen: string = 'unknown';
24
- private capturing: boolean = false;
25
- private running: boolean = false;
26
- private provider: ScreenshotProvider;
27
- private log: LogFn;
28
- private warn: LogFn;
29
-
30
- constructor(
31
- config: SnapshotCaptureConfig,
32
- onSnapshot: (snapshot: SnapshotPayload) => void,
33
- provider: ScreenshotProvider = captureScreenshot,
34
- log: LogFn = () => {},
35
- warn: LogFn = () => {},
36
- ) {
37
- this.config = config;
38
- this.onSnapshot = onSnapshot;
39
- this.provider = provider;
40
- this.log = log;
41
- this.warn = warn;
42
- }
43
-
44
- start(): void {
45
- if (this.running) return;
46
- this.running = true;
47
- this.log('Snapshot capture started (interval:', this.config.captureInterval, 'ms)');
48
- this.scheduleNext();
49
- }
50
-
51
- stop(): void {
52
- this.running = false;
53
- if (this.timeoutId !== null) {
54
- clearTimeout(this.timeoutId);
55
- this.timeoutId = null;
56
- }
57
- this.log('Snapshot capture stopped');
58
- }
59
-
60
- async captureNow(): Promise<void> {
61
- await this.tick();
62
- }
63
-
64
- private scheduleNext(): void {
65
- if (!this.running) return;
66
- this.timeoutId = setTimeout(() => {
67
- Promise.resolve(this.tick()).finally(() => {
68
- if (this.running) {
69
- this.scheduleNext();
70
- }
71
- });
72
- }, this.config.captureInterval);
73
- }
74
-
75
- private async tick(): Promise<void> {
76
- if (!this.sessionId) return;
77
- if (this.capturing) return;
78
-
79
- if (this.sequence >= this.config.maxSnapshotsPerSession) {
80
- this.log('Max snapshots reached:', this.sequence);
81
- this.stop();
82
- return;
83
- }
84
-
85
- this.capturing = true;
86
- let screenshot: ScreenshotData | null = null;
87
- try {
88
- screenshot = await this.provider();
89
- } catch (err) {
90
- this.warn('Snapshot capture failed:', err);
91
- screenshot = null;
92
- } finally {
93
- this.capturing = false;
94
- }
95
-
96
- if (!screenshot) {
97
- this.warn('Snapshot returned null (provider unavailable or failed)');
98
- return;
99
- }
100
-
101
- // Strip `source` field before sending — the backend DTO only
102
- // accepts { image, width, height, format } and rejects unknown props.
103
- const { image, width, height, format } = screenshot;
104
-
105
- const payload: SnapshotPayload = {
106
- sessionId: this.sessionId,
107
- screenName: this.currentScreen,
108
- timestamp: new Date().toISOString(),
109
- sequence: this.sequence,
110
- isDiff: false,
111
- data: { image, width, height, format },
112
- };
113
-
114
- this.sequence++;
115
- this.log(
116
- 'Snapshot captured: seq=', this.sequence - 1,
117
- 'screen=', this.currentScreen,
118
- 'size=', Math.round(screenshot.image.length / 1024), 'KB',
119
- );
120
- this.onSnapshot(payload);
121
- }
122
-
123
- reset(): void {
124
- this.sequence = 0;
125
- }
126
-
127
- setSessionId(sessionId: string | null): void {
128
- this.sessionId = sessionId;
129
- }
130
-
131
- setCurrentScreen(screenName: string): void {
132
- this.currentScreen = screenName;
133
- }
134
-
135
- getSequence(): number {
136
- return this.sequence;
137
- }
138
-
139
- isRunning(): boolean {
140
- return this.running;
141
- }
142
- }
1
+ import { SnapshotPayload, ScreenshotData } from '../types/snapshot';
2
+ import { captureScreenshot } from './captureScreenshot';
3
+
4
+ export interface SnapshotCaptureConfig {
5
+ captureInterval: number;
6
+ maxSnapshotsPerSession: number;
7
+ }
8
+
9
+ export type ScreenshotProvider = () => Promise<ScreenshotData | null>;
10
+
11
+ type LogFn = (...args: any[]) => void;
12
+
13
+ /**
14
+ * Captures JPEG screenshots of the RN view hierarchy at a fixed interval
15
+ * and forwards them to the host (usually the snapshot BatchQueue).
16
+ */
17
+ export class SnapshotCapture {
18
+ private timeoutId: ReturnType<typeof setTimeout> | null = null;
19
+ private sequence: number = 0;
20
+ private config: SnapshotCaptureConfig;
21
+ private onSnapshot: (snapshot: SnapshotPayload) => void;
22
+ private sessionId: string | null = null;
23
+ private currentScreen: string = 'unknown';
24
+ private capturing: boolean = false;
25
+ private running: boolean = false;
26
+ private provider: ScreenshotProvider;
27
+ private log: LogFn;
28
+ private warn: LogFn;
29
+
30
+ constructor(
31
+ config: SnapshotCaptureConfig,
32
+ onSnapshot: (snapshot: SnapshotPayload) => void,
33
+ provider: ScreenshotProvider = captureScreenshot,
34
+ log: LogFn = () => {},
35
+ warn: LogFn = () => {},
36
+ ) {
37
+ this.config = config;
38
+ this.onSnapshot = onSnapshot;
39
+ this.provider = provider;
40
+ this.log = log;
41
+ this.warn = warn;
42
+ }
43
+
44
+ /**
45
+ * Generation counter. Incremented on every start()/stop() so that timer
46
+ * chains from a PREVIOUS generation cannot reschedule themselves. Without
47
+ * it, a fast stop()→start() while a tick's capture is still in flight
48
+ * leaves two parallel timer chains alive (double capture rate).
49
+ */
50
+ private epoch = 0;
51
+
52
+ start(): void {
53
+ if (this.running) return;
54
+ this.running = true;
55
+ this.epoch++;
56
+ this.log('Snapshot capture started (interval:', this.config.captureInterval, 'ms)');
57
+ this.scheduleNext(this.epoch);
58
+ }
59
+
60
+ stop(): void {
61
+ this.running = false;
62
+ this.epoch++;
63
+ if (this.timeoutId !== null) {
64
+ clearTimeout(this.timeoutId);
65
+ this.timeoutId = null;
66
+ }
67
+ this.log('Snapshot capture stopped');
68
+ }
69
+
70
+ async captureNow(): Promise<void> {
71
+ await this.tick();
72
+ }
73
+
74
+ private scheduleNext(epoch: number): void {
75
+ if (!this.running || epoch !== this.epoch) return;
76
+ this.timeoutId = setTimeout(() => {
77
+ // Orphan timer from a previous generation — do nothing.
78
+ if (epoch !== this.epoch) return;
79
+ Promise.resolve(this.tick()).finally(() => {
80
+ if (this.running && epoch === this.epoch) {
81
+ this.scheduleNext(epoch);
82
+ }
83
+ });
84
+ }, this.config.captureInterval);
85
+ }
86
+
87
+ private async tick(): Promise<void> {
88
+ if (!this.sessionId) return;
89
+ if (this.capturing) return;
90
+
91
+ if (this.sequence >= this.config.maxSnapshotsPerSession) {
92
+ this.log('Max snapshots reached:', this.sequence);
93
+ this.stop();
94
+ return;
95
+ }
96
+
97
+ this.capturing = true;
98
+ let screenshot: ScreenshotData | null = null;
99
+ try {
100
+ screenshot = await this.provider();
101
+ } catch (err) {
102
+ this.warn('Snapshot capture failed:', err);
103
+ screenshot = null;
104
+ } finally {
105
+ this.capturing = false;
106
+ }
107
+
108
+ if (!screenshot) {
109
+ this.warn('Snapshot returned null (provider unavailable or failed)');
110
+ return;
111
+ }
112
+
113
+ // Strip `source` field before sending — the backend DTO only
114
+ // accepts { image, width, height, format } and rejects unknown props.
115
+ const { image, width, height, format } = screenshot;
116
+
117
+ const payload: SnapshotPayload = {
118
+ sessionId: this.sessionId,
119
+ screenName: this.currentScreen,
120
+ timestamp: new Date().toISOString(),
121
+ sequence: this.sequence,
122
+ isDiff: false,
123
+ data: { image, width, height, format },
124
+ };
125
+
126
+ this.sequence++;
127
+ this.log(
128
+ 'Snapshot captured: seq=', this.sequence - 1,
129
+ 'screen=', this.currentScreen,
130
+ 'size=', Math.round(screenshot.image.length / 1024), 'KB',
131
+ );
132
+ this.onSnapshot(payload);
133
+ }
134
+
135
+ reset(): void {
136
+ this.sequence = 0;
137
+ }
138
+
139
+ setSessionId(sessionId: string | null): void {
140
+ this.sessionId = sessionId;
141
+ }
142
+
143
+ setCurrentScreen(screenName: string): void {
144
+ this.currentScreen = screenName;
145
+ }
146
+
147
+ getSequence(): number {
148
+ return this.sequence;
149
+ }
150
+
151
+ isRunning(): boolean {
152
+ return this.running;
153
+ }
154
+ }