@repliqo/sdk-react-native 0.3.6 → 0.3.7

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.
@@ -79,6 +79,10 @@ exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumSc
79
79
  const scrollViewId = (0, react_1.useRef)(nextScrollViewId());
80
80
  const innerRef = (0, react_1.useRef)(null);
81
81
  const isMounted = (0, react_1.useRef)(true);
82
+ // Last measured window-relative top Y of the ScrollView — passed to
83
+ // captureScrollTile so the backend can crop each tile to just the
84
+ // scroll viewport area (no fixed header/footer duplication).
85
+ const lastViewportTop = (0, react_1.useRef)(null);
82
86
  const scrollState = (0, react_1.useRef)(null);
83
87
  // Forward the inner ref to the parent's ref (if provided).
84
88
  // IMPORTANT: do NOT clear innerRef on cleanup — RN sometimes calls the
@@ -112,6 +116,7 @@ exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumSc
112
116
  // ignore those, the next onLayout/onScroll will re-measure.
113
117
  if (width <= 0 || height <= 0)
114
118
  return;
119
+ lastViewportTop.current = y;
115
120
  try {
116
121
  client_1.AppAnalytics.getInstance().registerScrollView(scrollViewId.current, {
117
122
  x, y, width, height,
@@ -166,7 +171,8 @@ exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumSc
166
171
  lastCaptureY.current = state.offsetY;
167
172
  initialCaptured.current = true;
168
173
  try {
169
- client_1.AppAnalytics.getInstance().captureScrollTile(state.offsetY, state.viewportHeight, state.contentHeight);
174
+ const windowHeight = react_native_1.Dimensions.get('window').height;
175
+ client_1.AppAnalytics.getInstance().captureScrollTile(state.offsetY, state.viewportHeight, state.contentHeight, lastViewportTop.current ?? undefined, windowHeight);
170
176
  }
171
177
  catch { }
172
178
  };
@@ -85,11 +85,16 @@ export declare class AppAnalytics {
85
85
  * Capture the current viewport as a tile for collaborative screen
86
86
  * building. Called by RepliqoScrollView on scroll stops.
87
87
  *
88
- * The tile includes the scrollOffsetY so the backend knows where
89
- * in the full content this viewport belongs. Multiple users/sessions
90
- * contribute tiles until the full content is covered.
88
+ * The tile includes:
89
+ * - `scrollOffsetY`: where in the full content this viewport belongs
90
+ * - `viewportTop`: Y position (window logical px) of the ScrollView's
91
+ * top edge. Used by the backend to crop each tile to just the
92
+ * scrollable area, removing fixed headers/footers that would
93
+ * otherwise duplicate in the composite.
94
+ * - `windowHeight`: window height in logical px, for image-to-logical
95
+ * scale computation during backend cropping.
91
96
  */
92
- captureScrollTile(scrollOffsetY: number, viewportHeight: number, contentHeight: number): void;
97
+ captureScrollTile(scrollOffsetY: number, viewportHeight: number, contentHeight: number, viewportTop?: number, windowHeight?: number): void;
93
98
  /**
94
99
  * Programmatically scroll-scan the current ScrollView and upload all
95
100
  * tiles to the backend. Invisible to the user (~300-500ms).
@@ -331,11 +331,16 @@ class AppAnalytics {
331
331
  * Capture the current viewport as a tile for collaborative screen
332
332
  * building. Called by RepliqoScrollView on scroll stops.
333
333
  *
334
- * The tile includes the scrollOffsetY so the backend knows where
335
- * in the full content this viewport belongs. Multiple users/sessions
336
- * contribute tiles until the full content is covered.
334
+ * The tile includes:
335
+ * - `scrollOffsetY`: where in the full content this viewport belongs
336
+ * - `viewportTop`: Y position (window logical px) of the ScrollView's
337
+ * top edge. Used by the backend to crop each tile to just the
338
+ * scrollable area, removing fixed headers/footers that would
339
+ * otherwise duplicate in the composite.
340
+ * - `windowHeight`: window height in logical px, for image-to-logical
341
+ * scale computation during backend cropping.
337
342
  */
338
- captureScrollTile(scrollOffsetY, viewportHeight, contentHeight) {
343
+ captureScrollTile(scrollOffsetY, viewportHeight, contentHeight, viewportTop, windowHeight) {
339
344
  if (!this.sessionId || !this.currentScreen)
340
345
  return;
341
346
  if (viewportHeight <= 0)
@@ -350,8 +355,9 @@ class AppAnalytics {
350
355
  return;
351
356
  this.logger.log(`Scroll tile: "${screenName}" Y=${Math.round(scrollOffsetY)} ` +
352
357
  `vp=${Math.round(viewportHeight)} content=${Math.round(contentHeight)} ` +
358
+ `top=${viewportTop !== undefined ? Math.round(viewportTop) : '?'} ` +
353
359
  `${Math.round(frame.image.length / 1024)}KB`);
354
- await this.apiClient.uploadScreenTile(sessionId, screenName, frame.image, frame.width, frame.height, scrollOffsetY, viewportHeight, contentHeight);
360
+ await this.apiClient.uploadScreenTile(sessionId, screenName, frame.image, frame.width, frame.height, scrollOffsetY, viewportHeight, contentHeight, viewportTop, windowHeight);
355
361
  }
356
362
  catch (err) {
357
363
  this.logger.warn('Scroll tile failed:', err);
@@ -32,7 +32,14 @@ export declare class ApiClient {
32
32
  */
33
33
  /**
34
34
  * Upload a scroll tile for collaborative screen building.
35
+ *
36
+ * @param viewportTop Y position (window logical px) of the ScrollView's
37
+ * top edge. Used by the backend to crop each tile to just the
38
+ * scrollable area, removing fixed headers/footers that would
39
+ * otherwise duplicate in the composite.
40
+ * @param windowHeight Window height (logical px) at capture time.
41
+ * Used by the backend to compute image-to-logical scale for cropping.
35
42
  */
36
- uploadScreenTile(sessionId: string, screenName: string, image: string, width: number, height: number, scrollOffsetY: number, viewportHeight: number, contentHeight: number): Promise<void>;
43
+ uploadScreenTile(sessionId: string, screenName: string, image: string, width: number, height: number, scrollOffsetY: number, viewportHeight: number, contentHeight: number, viewportTop?: number, windowHeight?: number): Promise<void>;
37
44
  uploadScreenCapture(screenName: string, image: string, width: number, height: number, isFullContent: boolean): Promise<void>;
38
45
  }
@@ -158,22 +158,36 @@ class ApiClient {
158
158
  */
159
159
  /**
160
160
  * Upload a scroll tile for collaborative screen building.
161
+ *
162
+ * @param viewportTop Y position (window logical px) of the ScrollView's
163
+ * top edge. Used by the backend to crop each tile to just the
164
+ * scrollable area, removing fixed headers/footers that would
165
+ * otherwise duplicate in the composite.
166
+ * @param windowHeight Window height (logical px) at capture time.
167
+ * Used by the backend to compute image-to-logical scale for cropping.
161
168
  */
162
- async uploadScreenTile(sessionId, screenName, image, width, height, scrollOffsetY, viewportHeight, contentHeight) {
169
+ async uploadScreenTile(sessionId, screenName, image, width, height, scrollOffsetY, viewportHeight, contentHeight, viewportTop, windowHeight) {
163
170
  try {
171
+ const body = {
172
+ sessionId,
173
+ screenName,
174
+ image,
175
+ width,
176
+ height,
177
+ scrollOffsetY: Math.round(scrollOffsetY),
178
+ viewportHeight: Math.round(viewportHeight),
179
+ contentHeight: Math.round(contentHeight),
180
+ };
181
+ if (typeof viewportTop === 'number' && Number.isFinite(viewportTop)) {
182
+ body.viewportTop = Math.round(viewportTop);
183
+ }
184
+ if (typeof windowHeight === 'number' && Number.isFinite(windowHeight)) {
185
+ body.windowHeight = Math.round(windowHeight);
186
+ }
164
187
  const response = await fetch(`${this.baseUrl}/api/screen-tiles`, {
165
188
  method: 'POST',
166
189
  headers: this.getHeaders(),
167
- body: JSON.stringify({
168
- sessionId,
169
- screenName,
170
- image,
171
- width,
172
- height,
173
- scrollOffsetY: Math.round(scrollOffsetY),
174
- viewportHeight: Math.round(viewportHeight),
175
- contentHeight: Math.round(contentHeight),
176
- }),
190
+ body: JSON.stringify(body),
177
191
  });
178
192
  if (!response.ok) {
179
193
  const text = await response.text();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@repliqo/sdk-react-native",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Session replay & analytics SDK for React Native. Captures screenshots (including modals/alerts), navigation, screen visits, and custom events.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -2,6 +2,7 @@ import React, { useCallback, useRef, useEffect } from 'react';
2
2
  import {
3
3
  ScrollView,
4
4
  Keyboard,
5
+ Dimensions,
5
6
  type ScrollViewProps,
6
7
  type NativeSyntheticEvent,
7
8
  type NativeScrollEvent,
@@ -54,6 +55,10 @@ export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
54
55
  const scrollViewId = useRef<string>(nextScrollViewId());
55
56
  const innerRef = useRef<ScrollView | null>(null);
56
57
  const isMounted = useRef(true);
58
+ // Last measured window-relative top Y of the ScrollView — passed to
59
+ // captureScrollTile so the backend can crop each tile to just the
60
+ // scroll viewport area (no fixed header/footer duplication).
61
+ const lastViewportTop = useRef<number | null>(null);
57
62
  const scrollState = useRef<{
58
63
  offsetY: number;
59
64
  viewportHeight: number;
@@ -88,6 +93,7 @@ export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
88
93
  // RN sometimes returns 0/0 dims before the view is positioned —
89
94
  // ignore those, the next onLayout/onScroll will re-measure.
90
95
  if (width <= 0 || height <= 0) return;
96
+ lastViewportTop.current = y;
91
97
  try {
92
98
  AppAnalytics.getInstance().registerScrollView(scrollViewId.current, {
93
99
  x, y, width, height,
@@ -141,10 +147,13 @@ export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
141
147
  initialCaptured.current = true;
142
148
 
143
149
  try {
150
+ const windowHeight = Dimensions.get('window').height;
144
151
  AppAnalytics.getInstance().captureScrollTile(
145
152
  state.offsetY,
146
153
  state.viewportHeight,
147
154
  state.contentHeight,
155
+ lastViewportTop.current ?? undefined,
156
+ windowHeight,
148
157
  );
149
158
  } catch {}
150
159
  };
@@ -446,14 +446,21 @@ export class AppAnalytics {
446
446
  * Capture the current viewport as a tile for collaborative screen
447
447
  * building. Called by RepliqoScrollView on scroll stops.
448
448
  *
449
- * The tile includes the scrollOffsetY so the backend knows where
450
- * in the full content this viewport belongs. Multiple users/sessions
451
- * contribute tiles until the full content is covered.
449
+ * The tile includes:
450
+ * - `scrollOffsetY`: where in the full content this viewport belongs
451
+ * - `viewportTop`: Y position (window logical px) of the ScrollView's
452
+ * top edge. Used by the backend to crop each tile to just the
453
+ * scrollable area, removing fixed headers/footers that would
454
+ * otherwise duplicate in the composite.
455
+ * - `windowHeight`: window height in logical px, for image-to-logical
456
+ * scale computation during backend cropping.
452
457
  */
453
458
  captureScrollTile(
454
459
  scrollOffsetY: number,
455
460
  viewportHeight: number,
456
461
  contentHeight: number,
462
+ viewportTop?: number,
463
+ windowHeight?: number,
457
464
  ): void {
458
465
  if (!this.sessionId || !this.currentScreen) return;
459
466
  if (viewportHeight <= 0) return; // No real dimensions yet
@@ -470,6 +477,7 @@ export class AppAnalytics {
470
477
  this.logger.log(
471
478
  `Scroll tile: "${screenName}" Y=${Math.round(scrollOffsetY)} ` +
472
479
  `vp=${Math.round(viewportHeight)} content=${Math.round(contentHeight)} ` +
480
+ `top=${viewportTop !== undefined ? Math.round(viewportTop) : '?'} ` +
473
481
  `${Math.round(frame.image.length / 1024)}KB`,
474
482
  );
475
483
 
@@ -482,6 +490,8 @@ export class AppAnalytics {
482
490
  scrollOffsetY,
483
491
  viewportHeight,
484
492
  contentHeight,
493
+ viewportTop,
494
+ windowHeight,
485
495
  );
486
496
  } catch (err) {
487
497
  this.logger.warn('Scroll tile failed:', err);
@@ -229,6 +229,13 @@ export class ApiClient {
229
229
  */
230
230
  /**
231
231
  * Upload a scroll tile for collaborative screen building.
232
+ *
233
+ * @param viewportTop Y position (window logical px) of the ScrollView's
234
+ * top edge. Used by the backend to crop each tile to just the
235
+ * scrollable area, removing fixed headers/footers that would
236
+ * otherwise duplicate in the composite.
237
+ * @param windowHeight Window height (logical px) at capture time.
238
+ * Used by the backend to compute image-to-logical scale for cropping.
232
239
  */
233
240
  async uploadScreenTile(
234
241
  sessionId: string,
@@ -239,23 +246,33 @@ export class ApiClient {
239
246
  scrollOffsetY: number,
240
247
  viewportHeight: number,
241
248
  contentHeight: number,
249
+ viewportTop?: number,
250
+ windowHeight?: number,
242
251
  ): Promise<void> {
243
252
  try {
253
+ const body: Record<string, unknown> = {
254
+ sessionId,
255
+ screenName,
256
+ image,
257
+ width,
258
+ height,
259
+ scrollOffsetY: Math.round(scrollOffsetY),
260
+ viewportHeight: Math.round(viewportHeight),
261
+ contentHeight: Math.round(contentHeight),
262
+ };
263
+ if (typeof viewportTop === 'number' && Number.isFinite(viewportTop)) {
264
+ body.viewportTop = Math.round(viewportTop);
265
+ }
266
+ if (typeof windowHeight === 'number' && Number.isFinite(windowHeight)) {
267
+ body.windowHeight = Math.round(windowHeight);
268
+ }
269
+
244
270
  const response = await fetch(
245
271
  `${this.baseUrl}/api/screen-tiles`,
246
272
  {
247
273
  method: 'POST',
248
274
  headers: this.getHeaders(),
249
- body: JSON.stringify({
250
- sessionId,
251
- screenName,
252
- image,
253
- width,
254
- height,
255
- scrollOffsetY: Math.round(scrollOffsetY),
256
- viewportHeight: Math.round(viewportHeight),
257
- contentHeight: Math.round(contentHeight),
258
- }),
275
+ body: JSON.stringify(body),
259
276
  },
260
277
  );
261
278