@scalebun/react-native 1.10.7 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +25 -24
- package/dist/scalebun.full.js +236 -187
- package/dist/scalebun.slim.js +235 -186
- package/ios/Capture/InteractionTracker.swift +8 -4
- package/lib/commonjs/analytics/EventTracker.js +5 -5
- package/lib/commonjs/analytics/automaticEvents.js +3 -2
- package/lib/commonjs/core/constants/version.js +7 -2
- package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +80 -103
- package/lib/commonjs/features/journey/interactionProtocol.js +47 -0
- package/lib/commonjs/features/journey/uiState.js +8 -1
- package/lib/commonjs/features/session/JourneyEventPipeline.js +6 -5
- package/lib/commonjs/features/session/SessionManager.js +37 -38
- package/lib/module/analytics/EventTracker.js +5 -5
- package/lib/module/analytics/automaticEvents.js +3 -2
- package/lib/module/core/constants/version.js +7 -2
- package/lib/module/features/journey/ScaleBunDebugRoot.js +80 -103
- package/lib/module/features/journey/interactionProtocol.js +38 -0
- package/lib/module/features/journey/uiState.js +8 -1
- package/lib/module/features/session/JourneyEventPipeline.js +6 -5
- package/lib/module/features/session/SessionManager.js +37 -38
- package/lib/typescript/analytics/EventTracker.d.ts +1 -1
- package/lib/typescript/analytics/automaticEvents.d.ts +3 -1
- package/lib/typescript/core/constants/version.d.ts +7 -2
- package/lib/typescript/features/journey/interactionProtocol.d.ts +21 -0
- package/lib/typescript/features/session/JourneyEventPipeline.d.ts +1 -0
- package/lib/typescript/features/session/SessionManager.d.ts +15 -10
- package/package.json +3 -2
- package/src/analytics/EventTracker.ts +5 -5
- package/src/analytics/automaticEvents.ts +4 -0
- package/src/core/constants/version.ts +7 -2
- package/src/features/journey/ScaleBunDebugRoot.tsx +96 -97
- package/src/features/journey/interactionProtocol.ts +65 -0
- package/src/features/journey/uiState.ts +9 -4
- package/src/features/session/JourneyEventPipeline.ts +7 -5
- package/src/features/session/SessionManager.ts +75 -38
|
@@ -41,16 +41,16 @@ import { bridgeAdapter } from '../replay/bridge/adapters/bridgeAdapter';
|
|
|
41
41
|
import { redactBody } from '../../debug/redaction';
|
|
42
42
|
import { consumeCalibrationTarget } from '../journey/calibrationContext';
|
|
43
43
|
import { resolvePlatformOS } from '../../core/context/device';
|
|
44
|
+
import { emitAutomaticEvent } from '../../analytics/automaticEvents';
|
|
45
|
+
import {
|
|
46
|
+
INTERACTION_PROTOCOL_VERSION,
|
|
47
|
+
automaticInteractionProperties,
|
|
48
|
+
generateInteractionId,
|
|
49
|
+
type InteractionStateStatus,
|
|
50
|
+
} from '../journey/interactionProtocol';
|
|
44
51
|
|
|
45
52
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
46
53
|
|
|
47
|
-
/**
|
|
48
|
-
* Max analytics-lane heatmap interactions emitted per foreground (analytics
|
|
49
|
-
* session) window. Bounds ingest volume now that capture defaults ON; enough to
|
|
50
|
-
* resolve hotspot density, the long tail is dropped (drop-newest beyond cap).
|
|
51
|
-
*/
|
|
52
|
-
const HEATMAP_MAX_INTERACTIONS_PER_WINDOW = 200;
|
|
53
|
-
|
|
54
54
|
export interface SessionManagerConfig {
|
|
55
55
|
sessionReplay?: Partial<SessionReplayConfig>;
|
|
56
56
|
syncPolicy?: SyncDecisionCallback;
|
|
@@ -84,16 +84,6 @@ export class SessionManager {
|
|
|
84
84
|
* lane even when no replay recording is active. Additive, opt-in (default off).
|
|
85
85
|
*/
|
|
86
86
|
private _captureInteractionHeatmap = false;
|
|
87
|
-
/**
|
|
88
|
-
* Sampling cap for the analytics-lane heatmap emission. Now that capture is
|
|
89
|
-
* ON by default, an unbounded one-event-per-gesture stream could materially
|
|
90
|
-
* inflate ingest volume. We cap emitted interactions per analytics-session
|
|
91
|
-
* window (finalize-scoped per foreground): the first N gestures define the
|
|
92
|
-
* hotspot shape; the long tail is dropped. Resets when the window changes.
|
|
93
|
-
*/
|
|
94
|
-
private _heatmapWindowSessionId: string | null = null;
|
|
95
|
-
private _heatmapWindowCount = 0;
|
|
96
|
-
|
|
97
87
|
private session: Session | null = null;
|
|
98
88
|
private active = false;
|
|
99
89
|
private timeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -740,21 +730,16 @@ export class SessionManager {
|
|
|
740
730
|
* injected sensitive keys (e.g. a label) are stripped before transport.
|
|
741
731
|
* - Never throws.
|
|
742
732
|
*/
|
|
743
|
-
private _emitInteractionToAnalytics(
|
|
744
|
-
|
|
745
|
-
|
|
733
|
+
private _emitInteractionToAnalytics(
|
|
734
|
+
gestureType: string,
|
|
735
|
+
payload: Record<string, unknown>,
|
|
736
|
+
occurredAt: number,
|
|
737
|
+
screenName?: string,
|
|
738
|
+
): boolean {
|
|
739
|
+
if (!this._captureInteractionHeatmap) return false;
|
|
740
|
+
if (this.active) return false; // recording lane already carries this tap
|
|
746
741
|
const adapter = this._backendTransport;
|
|
747
|
-
if (!adapter) return;
|
|
748
|
-
|
|
749
|
-
// Per-foreground sampling cap. Reset the counter when the analytics
|
|
750
|
-
// session window rolls over, then drop anything past the cap.
|
|
751
|
-
const windowId = adapter.analyticsSessionId ?? '';
|
|
752
|
-
if (windowId !== this._heatmapWindowSessionId) {
|
|
753
|
-
this._heatmapWindowSessionId = windowId;
|
|
754
|
-
this._heatmapWindowCount = 0;
|
|
755
|
-
}
|
|
756
|
-
if (this._heatmapWindowCount >= HEATMAP_MAX_INTERACTIONS_PER_WINDOW) return;
|
|
757
|
-
this._heatmapWindowCount++;
|
|
742
|
+
if (!adapter) return false;
|
|
758
743
|
|
|
759
744
|
try {
|
|
760
745
|
// Normalize coords in place (same logic as the recording lane).
|
|
@@ -782,16 +767,18 @@ export class SessionManager {
|
|
|
782
767
|
eventId: generateEventId(),
|
|
783
768
|
// sessionId is (re)stamped by the analytics lane at flush time.
|
|
784
769
|
sessionId: adapter.analyticsSessionId ?? '',
|
|
785
|
-
ts:
|
|
770
|
+
ts: occurredAt,
|
|
786
771
|
type: 'USER_ACTION',
|
|
787
772
|
subtype: `gesture:${gestureType}`,
|
|
788
|
-
screen: this._lastKnownScreen ?? undefined,
|
|
773
|
+
screen: screenName ?? this._lastKnownScreen ?? undefined,
|
|
789
774
|
payload: safePayload,
|
|
790
775
|
source: 'user',
|
|
791
776
|
};
|
|
792
777
|
adapter.trackEvent(event);
|
|
778
|
+
return true;
|
|
793
779
|
} catch (err) {
|
|
794
780
|
logger.error('[SessionManager] heatmap analytics emit failed:', err);
|
|
781
|
+
return false;
|
|
795
782
|
}
|
|
796
783
|
}
|
|
797
784
|
|
|
@@ -810,6 +797,8 @@ export class SessionManager {
|
|
|
810
797
|
traceId?: string;
|
|
811
798
|
source?: JourneyEventSource;
|
|
812
799
|
journeyId?: string;
|
|
800
|
+
/** Original observation time; interaction capture may resolve asynchronously. */
|
|
801
|
+
timestamp?: number;
|
|
813
802
|
},
|
|
814
803
|
): void {
|
|
815
804
|
if (!this.active) return;
|
|
@@ -1046,13 +1035,18 @@ export class SessionManager {
|
|
|
1046
1035
|
* Notify of a user interaction. Called by ScaleBunDebugRoot touch handlers.
|
|
1047
1036
|
* Also triggers frame capture for desktop-initiated recordings.
|
|
1048
1037
|
*/
|
|
1049
|
-
onUserAction(
|
|
1038
|
+
onUserAction(
|
|
1039
|
+
subtype: string,
|
|
1040
|
+
payload?: Record<string, unknown>,
|
|
1041
|
+
context?: { screen?: string; timestamp?: number },
|
|
1042
|
+
): void {
|
|
1050
1043
|
if (!this.active) return; // Defense-in-depth: no emission when recording is OFF
|
|
1051
1044
|
this.emitEvent('USER_ACTION', {
|
|
1052
1045
|
subtype,
|
|
1053
|
-
screen: this._lastKnownScreen ?? undefined,
|
|
1046
|
+
screen: context?.screen ?? this._lastKnownScreen ?? undefined,
|
|
1054
1047
|
payload,
|
|
1055
1048
|
source: 'user',
|
|
1049
|
+
timestamp: context?.timestamp,
|
|
1056
1050
|
});
|
|
1057
1051
|
// NOTE: Do NOT call captureManager.onInteraction() here.
|
|
1058
1052
|
// emitEvent() already triggers onInteraction() for USER_ACTION events (line ~405).
|
|
@@ -1103,11 +1097,22 @@ export class SessionManager {
|
|
|
1103
1097
|
screenWidth?: number;
|
|
1104
1098
|
screenHeight?: number;
|
|
1105
1099
|
platform?: string;
|
|
1100
|
+
interactionId?: string;
|
|
1101
|
+
interactionProtocol?: number;
|
|
1102
|
+
occurredAt?: number;
|
|
1103
|
+
ui?: string;
|
|
1104
|
+
stateStatus?: InteractionStateStatus;
|
|
1105
|
+
targetId?: string;
|
|
1106
|
+
screenName?: string;
|
|
1107
|
+
emitAutomaticAnalytics?: boolean;
|
|
1106
1108
|
}): void {
|
|
1107
1109
|
// Exact-duplicate suppression — see _lastGestureSig. Signature is the gesture type plus
|
|
1108
1110
|
// raw coordinates verbatim; String() keeps undefined coords distinct from 0 (a payload
|
|
1109
1111
|
// with no coords never collides with a real origin tap).
|
|
1110
|
-
const
|
|
1112
|
+
const suppliedInteractionId = details?.interactionId;
|
|
1113
|
+
const dedupSig = suppliedInteractionId
|
|
1114
|
+
? `id:${suppliedInteractionId}`
|
|
1115
|
+
: `${gestureType}|${String(details?.x)}|${String(details?.y)}|${String(details?.endX)}|${String(details?.endY)}`;
|
|
1111
1116
|
const nowTs = Date.now();
|
|
1112
1117
|
if (
|
|
1113
1118
|
dedupSig === this._lastGestureSig &&
|
|
@@ -1122,7 +1127,15 @@ export class SessionManager {
|
|
|
1122
1127
|
// Build the canonical gesture payload ONCE so the recording lane and the
|
|
1123
1128
|
// (additive) analytics lane carry byte-identical keys (normalizedX/Y,
|
|
1124
1129
|
// gestureType, etc.). Same object shape that was previously inlined.
|
|
1130
|
+
const interactionId = suppliedInteractionId ?? generateInteractionId();
|
|
1131
|
+
const occurredAt = details?.occurredAt ?? Date.now();
|
|
1132
|
+
const stateStatus = details?.stateStatus ?? (details?.ui ? 'captured_nonempty' : 'not_captured');
|
|
1125
1133
|
const payload: Record<string, unknown> = {
|
|
1134
|
+
interaction_id: interactionId,
|
|
1135
|
+
interaction_protocol: details?.interactionProtocol ?? INTERACTION_PROTOCOL_VERSION,
|
|
1136
|
+
state_status: stateStatus,
|
|
1137
|
+
ui: details?.ui,
|
|
1138
|
+
target_id: details?.targetId,
|
|
1126
1139
|
gestureType,
|
|
1127
1140
|
x: details?.x,
|
|
1128
1141
|
y: details?.y,
|
|
@@ -1167,7 +1180,27 @@ export class SessionManager {
|
|
|
1167
1180
|
// when the flag is OFF, no backend transport is attached, or recording is
|
|
1168
1181
|
// active (the recording path below already carries this tap). Uses a fresh
|
|
1169
1182
|
// payload clone so analytics normalization never mutates the recording one.
|
|
1170
|
-
this._emitInteractionToAnalytics(
|
|
1183
|
+
const analyticsReplayCarrier = this._emitInteractionToAnalytics(
|
|
1184
|
+
gestureType,
|
|
1185
|
+
{ ...payload },
|
|
1186
|
+
occurredAt,
|
|
1187
|
+
details?.screenName,
|
|
1188
|
+
);
|
|
1189
|
+
const replayCarrier = !!this._backendTransport && (this.active || analyticsReplayCarrier);
|
|
1190
|
+
if (details?.emitAutomaticAnalytics) {
|
|
1191
|
+
try {
|
|
1192
|
+
const safePayload = redactBody(payload) as Record<string, unknown>;
|
|
1193
|
+
emitAutomaticEvent(
|
|
1194
|
+
'element_interacted',
|
|
1195
|
+
automaticInteractionProperties(
|
|
1196
|
+
safePayload,
|
|
1197
|
+
details?.screenName ?? this._lastKnownScreen ?? undefined,
|
|
1198
|
+
replayCarrier,
|
|
1199
|
+
),
|
|
1200
|
+
occurredAt,
|
|
1201
|
+
);
|
|
1202
|
+
} catch { /* automatic projection must never affect interaction capture */ }
|
|
1203
|
+
}
|
|
1171
1204
|
|
|
1172
1205
|
if (!this.active) return; // Defense-in-depth: no emission when recording is OFF
|
|
1173
1206
|
// The subtype becomes `ReplayEvent.label`, which is what the Events explorer GROUPS BY. Naming
|
|
@@ -1178,7 +1211,11 @@ export class SessionManager {
|
|
|
1178
1211
|
// ⚠️ `gestureType` also travels in the payload, and the backend's `resolveGesture` reads THAT
|
|
1179
1212
|
// first — so enriching the label cannot change heatmap gesture classification.
|
|
1180
1213
|
const target = typeof details?.target === 'string' ? details.target.trim() : '';
|
|
1181
|
-
this.onUserAction(
|
|
1214
|
+
this.onUserAction(
|
|
1215
|
+
target ? `gesture:${gestureType} · ${target}` : `gesture:${gestureType}`,
|
|
1216
|
+
payload,
|
|
1217
|
+
{ screen: details?.screenName, timestamp: occurredAt },
|
|
1218
|
+
);
|
|
1182
1219
|
}
|
|
1183
1220
|
|
|
1184
1221
|
/**
|