sitepong 0.2.11 → 0.2.13
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/dist/entries/rn.d.ts +6 -0
- package/dist/entries/rn.js +56 -3
- package/dist/entries/rn.js.map +1 -1
- package/ios/ScreenRecorder/ScreenRecorderModule.swift +3 -1
- package/ios/WatchtowerCore/TapEvent.swift +10 -0
- package/ios/WatchtowerCore/Watchtower.swift +3 -3
- package/ios/WatchtowerCore/WatchtowerEngine.swift +81 -5
- package/package.json +1 -1
|
@@ -121,10 +121,12 @@ public class SitePongScreenRecorderModule: Module {
|
|
|
121
121
|
let sampleRate = config["sampleRate"] as? Double ?? 0.1
|
|
122
122
|
// RN host stamps platform "react-native-ios" (§1.1/§4.4).
|
|
123
123
|
let platform = config["platform"] as? String ?? "react-native-ios"
|
|
124
|
+
// Idle grace before a reopen rolls a new session (default 30s).
|
|
125
|
+
let sessionGraceMs = config["sessionGraceMs"] as? Double ?? 30_000
|
|
124
126
|
guard let endpoint = URL(string: endpointStr) else { return }
|
|
125
127
|
Watchtower.start(apiKey: apiKey, projectId: projectId,
|
|
126
128
|
endpoint: endpoint, sampleRate: sampleRate,
|
|
127
|
-
platform: platform)
|
|
129
|
+
platform: platform, sessionGraceMs: sessionGraceMs)
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
Function("watchtowerStop") {
|
|
@@ -14,6 +14,16 @@ public struct CaptureEvent: Codable {
|
|
|
14
14
|
public var distinct_id: String?
|
|
15
15
|
public var device_model: String?
|
|
16
16
|
public var os_version: String?
|
|
17
|
+
/// Persistent per-install id (identifierForVendor) — stamped on every event
|
|
18
|
+
/// so the dashboard can stitch consecutive sessions from the same install
|
|
19
|
+
/// into a journey at read time, even for anonymous users.
|
|
20
|
+
public var install_id: String?
|
|
21
|
+
|
|
22
|
+
// session_start (only on a session that opened after a background→foreground
|
|
23
|
+
// gap): the id of the session this one continues from, and the idle gap in
|
|
24
|
+
// milliseconds. These are the read-time linking hints.
|
|
25
|
+
public var previous_session_id: String?
|
|
26
|
+
public var session_gap_ms: UInt64?
|
|
17
27
|
|
|
18
28
|
// identity traits — only populated on an "identify" event (Watchtower.setUser
|
|
19
29
|
// with email/name), so PII is sent once per identify, not on every tap.
|
|
@@ -13,18 +13,18 @@ public enum Watchtower {
|
|
|
13
13
|
/// identity knob — capture logic is identical regardless of platform.
|
|
14
14
|
public static func start(apiKey: String, projectId: String,
|
|
15
15
|
endpoint: URL, sampleRate: Double = 0.1,
|
|
16
|
-
platform: String = "ios") {
|
|
16
|
+
platform: String = "ios", sessionGraceMs: Double = 30_000) {
|
|
17
17
|
#if canImport(UIKit)
|
|
18
18
|
// Ensure UIKit work happens on the main thread.
|
|
19
19
|
if Thread.isMainThread {
|
|
20
20
|
WatchtowerEngine.shared.start(apiKey: apiKey, projectId: projectId,
|
|
21
21
|
endpoint: endpoint, sampleRate: sampleRate,
|
|
22
|
-
platform: platform)
|
|
22
|
+
platform: platform, sessionGraceMs: sessionGraceMs)
|
|
23
23
|
} else {
|
|
24
24
|
DispatchQueue.main.async {
|
|
25
25
|
WatchtowerEngine.shared.start(apiKey: apiKey, projectId: projectId,
|
|
26
26
|
endpoint: endpoint, sampleRate: sampleRate,
|
|
27
|
-
platform: platform)
|
|
27
|
+
platform: platform, sessionGraceMs: sessionGraceMs)
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
#endif
|
|
@@ -37,6 +37,22 @@ final class WatchtowerEngine {
|
|
|
37
37
|
/// overrides to "react-native-ios" via start(platform:).
|
|
38
38
|
private var platform: String = "ios"
|
|
39
39
|
|
|
40
|
+
/// Persistent per-install id (identifierForVendor), resolved once at start
|
|
41
|
+
/// and stamped on every event so the dashboard can stitch consecutive
|
|
42
|
+
/// sessions from the same install into a journey at read time.
|
|
43
|
+
private var installId: String?
|
|
44
|
+
/// Wall-clock of the last background transition (nil until first background).
|
|
45
|
+
/// Drives the reopen-is-a-new-session decision on the next foreground.
|
|
46
|
+
private var lastBackgroundedAt: Date?
|
|
47
|
+
/// Idle grace: a foreground within this window of the last background resumes
|
|
48
|
+
/// the SAME session (a momentary app-switch shouldn't fragment); beyond it, a
|
|
49
|
+
/// fresh session is rolled. Default 30s; overridable via start(sessionGraceMs:).
|
|
50
|
+
private var sessionGraceMs: Double = 30_000
|
|
51
|
+
/// On a rolled session these carry the prior session id + idle gap (ms); they
|
|
52
|
+
/// ride the new session's session_start, then clear.
|
|
53
|
+
private var pendingPreviousSessionId: String?
|
|
54
|
+
private var pendingSessionGapMs: UInt64?
|
|
55
|
+
|
|
40
56
|
/// Explicit screen identity override via setScreen (§3.2). Cleared/overridden
|
|
41
57
|
/// by viewDidAppear unless re-set.
|
|
42
58
|
private var explicitScreenName: String?
|
|
@@ -67,13 +83,18 @@ final class WatchtowerEngine {
|
|
|
67
83
|
// MARK: - Lifecycle
|
|
68
84
|
|
|
69
85
|
func start(apiKey: String, projectId: String, endpoint: URL, sampleRate: Double,
|
|
70
|
-
platform: String = "ios") {
|
|
86
|
+
platform: String = "ios", sessionGraceMs: Double = 30_000) {
|
|
71
87
|
guard !isRunning else { return }
|
|
72
88
|
self.transport = Transport(apiKey: apiKey, projectId: projectId, endpoint: endpoint)
|
|
73
89
|
self.sessionId = UUID().uuidString
|
|
74
90
|
self.sequence = 0
|
|
75
91
|
self.sampleRate = sampleRate
|
|
76
92
|
self.platform = platform
|
|
93
|
+
self.sessionGraceMs = sessionGraceMs
|
|
94
|
+
self.installId = UIDevice.current.identifierForVendor?.uuidString
|
|
95
|
+
self.lastBackgroundedAt = nil
|
|
96
|
+
self.pendingPreviousSessionId = nil
|
|
97
|
+
self.pendingSessionGapMs = nil
|
|
77
98
|
self.uploadedScreenFps.removeAll()
|
|
78
99
|
self.inFlightScreenFps.removeAll()
|
|
79
100
|
self.fpCache.removeAll()
|
|
@@ -130,16 +151,29 @@ final class WatchtowerEngine {
|
|
|
130
151
|
/// per-session sequence.
|
|
131
152
|
private func baseEvent(type: String) -> CaptureEvent {
|
|
132
153
|
sequence += 1
|
|
133
|
-
|
|
154
|
+
var event = CaptureEvent(
|
|
134
155
|
type: type, session_id: sessionId, sequence: sequence,
|
|
135
156
|
timestamp: ISO8601DateFormatter.wt.string(from: Date()),
|
|
136
157
|
platform: platform, app_version: appVersion,
|
|
137
158
|
distinct_id: distinctId, device_model: deviceModel,
|
|
138
159
|
os_version: osVersion
|
|
139
160
|
)
|
|
161
|
+
event.install_id = installId
|
|
162
|
+
// Link hints ride only the session_start of a rolled session.
|
|
163
|
+
if type == "session_start" {
|
|
164
|
+
event.previous_session_id = pendingPreviousSessionId
|
|
165
|
+
event.session_gap_ms = pendingSessionGapMs
|
|
166
|
+
}
|
|
167
|
+
return event
|
|
140
168
|
}
|
|
141
169
|
|
|
170
|
+
/// Diagnostic seam: when set, receives every emitted event before it is
|
|
171
|
+
/// enqueued for transport. nil in production; used by tests to assert the
|
|
172
|
+
/// session lifecycle + linking fields without a network round-trip.
|
|
173
|
+
var debugOnEmit: ((CaptureEvent) -> Void)?
|
|
174
|
+
|
|
142
175
|
private func emit(_ event: CaptureEvent) {
|
|
176
|
+
debugOnEmit?(event)
|
|
143
177
|
transport?.enqueue(event)
|
|
144
178
|
}
|
|
145
179
|
|
|
@@ -165,14 +199,20 @@ final class WatchtowerEngine {
|
|
|
165
199
|
NotificationCenter.default.addObserver(
|
|
166
200
|
self, selector: #selector(appDidEnterBackground),
|
|
167
201
|
name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
202
|
+
NotificationCenter.default.addObserver(
|
|
203
|
+
self, selector: #selector(appWillEnterForeground),
|
|
204
|
+
name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
168
205
|
}
|
|
169
206
|
|
|
170
207
|
@objc private func appDidEnterBackground() {
|
|
171
208
|
// Pending dead-checks can't complete in the background — flush them
|
|
172
|
-
// undetermined (dead omitted), then mark the session boundary
|
|
173
|
-
//
|
|
174
|
-
//
|
|
209
|
+
// undetermined (dead omitted), then mark the (soft) session boundary and
|
|
210
|
+
// record when we backgrounded so the next foreground can decide whether
|
|
211
|
+
// this was a momentary switch (resume) or a real reopen (roll). The
|
|
212
|
+
// session does NOT end here: reason "background" lets the server close it
|
|
213
|
+
// on timeout if the app never returns.
|
|
175
214
|
flushPendingTaps()
|
|
215
|
+
lastBackgroundedAt = Date()
|
|
176
216
|
var end = baseEvent(type: "session_end")
|
|
177
217
|
end.reason = "background"
|
|
178
218
|
end.duration_ms = durationMs()
|
|
@@ -180,6 +220,42 @@ final class WatchtowerEngine {
|
|
|
180
220
|
transport?.flush()
|
|
181
221
|
}
|
|
182
222
|
|
|
223
|
+
/// A reopen. If we were backgrounded longer than the idle grace, the old
|
|
224
|
+
/// session is done — roll a fresh one (linked to the old via prev id + gap).
|
|
225
|
+
/// Within the grace it was just an app-switch, so the same session resumes.
|
|
226
|
+
@objc private func appWillEnterForeground() {
|
|
227
|
+
guard isRunning, let backgroundedAt = lastBackgroundedAt else { return }
|
|
228
|
+
let gapMs = Date().timeIntervalSince(backgroundedAt) * 1000
|
|
229
|
+
lastBackgroundedAt = nil
|
|
230
|
+
guard gapMs > sessionGraceMs else { return }
|
|
231
|
+
rollSession(gapMs: UInt64(max(0, gapMs)))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// Open a brand-new session on the same install. Mirrors start()'s
|
|
235
|
+
/// per-session reset but keeps transport, identity, device, and install id.
|
|
236
|
+
/// The prior session already emitted a session_end (reason "background") when
|
|
237
|
+
/// it backgrounded, so we only open the new one here.
|
|
238
|
+
private func rollSession(gapMs: UInt64) {
|
|
239
|
+
pendingPreviousSessionId = sessionId
|
|
240
|
+
pendingSessionGapMs = gapMs
|
|
241
|
+
sessionId = UUID().uuidString
|
|
242
|
+
sequence = 0
|
|
243
|
+
sessionStartedAt = Date()
|
|
244
|
+
isSampledSession = Double.random(in: 0..<1) < sampleRate
|
|
245
|
+
uploadedScreenFps.removeAll()
|
|
246
|
+
inFlightScreenFps.removeAll()
|
|
247
|
+
fpCache.removeAll()
|
|
248
|
+
lastCaptureAt.removeAll()
|
|
249
|
+
capturesInFlight.removeAll()
|
|
250
|
+
lastFrameHash = nil
|
|
251
|
+
lastEmittedScreenName = nil
|
|
252
|
+
navCounter = 0
|
|
253
|
+
pendingTaps.removeAll()
|
|
254
|
+
emit(baseEvent(type: "session_start"))
|
|
255
|
+
pendingPreviousSessionId = nil
|
|
256
|
+
pendingSessionGapMs = nil
|
|
257
|
+
}
|
|
258
|
+
|
|
183
259
|
// MARK: - Screen views (§1.1 screen_view)
|
|
184
260
|
|
|
185
261
|
/// Single choke point for screen identity changes — called with the
|