sitepong 0.2.8 → 0.2.9

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.
@@ -31,6 +31,14 @@ public final class SitePongStructuralCapture {
31
31
  private var flushTimer: Timer?
32
32
  private var running = false
33
33
 
34
+ // Incremental delivery: count of events CONFIRMED stored (== next seq_start).
35
+ // The device ships only the delta each flush and clears its buffer, so memory
36
+ // stays bounded and the uplink never re-sends the whole session. Node ids
37
+ // (`ids`/`nextId`) stay PERSISTENT across flushes — the server stitches the
38
+ // deltas by id, so resetting them would corrupt the tree.
39
+ private var flushedEventCount = 0
40
+ private var sending = false
41
+
34
42
  private func now() -> Int { Int(Date().timeIntervalSince(startedAt) * 1000) }
35
43
 
36
44
  /// Start capturing. `endpoint` is the ingest base URL (e.g.
@@ -57,6 +65,17 @@ public final class SitePongStructuralCapture {
57
65
  }
58
66
  sampleTimer = Timer.scheduledTimer(withTimeInterval: 0.12, repeats: true) { [weak self] _ in self?.sample() }
59
67
  flushTimer = Timer.scheduledTimer(withTimeInterval: 4.0, repeats: true) { [weak self] _ in self?.flush() }
68
+
69
+ // Flush on background so the tail isn't stranded when timers suspend.
70
+ NotificationCenter.default.addObserver(
71
+ self, selector: #selector(onBackground),
72
+ name: UIApplication.didEnterBackgroundNotification, object: nil
73
+ )
74
+ }
75
+
76
+ @objc private func onBackground() {
77
+ guard running else { return }
78
+ flush()
60
79
  }
61
80
 
62
81
  public func setScreen(_ name: String) {
@@ -71,6 +90,9 @@ public final class SitePongStructuralCapture {
71
90
  running = false
72
91
  sampleTimer?.invalidate(); sampleTimer = nil
73
92
  flushTimer?.invalidate(); flushTimer = nil
93
+ NotificationCenter.default.removeObserver(
94
+ self, name: UIApplication.didEnterBackgroundNotification, object: nil
95
+ )
74
96
  flush()
75
97
  }
76
98
 
@@ -171,23 +193,60 @@ public final class SitePongStructuralCapture {
171
193
  return v
172
194
  }
173
195
 
196
+ /// Ship the pending event delta (only what's accumulated since the last
197
+ /// confirmed flush), then clear the device buffer. The batch carries its
198
+ /// `seq_start` so the server dedups retries; a failed batch is re-queued so
199
+ /// nothing is lost offline. All buffer mutation stays on the main thread.
174
200
  private func flush() {
175
- guard !endpoint.isEmpty, !events.isEmpty, let w = keyWindow() else { return }
201
+ guard !endpoint.isEmpty, !sending, !events.isEmpty, let w = keyWindow() else { return }
202
+ let batch = events
203
+ let seqStart = flushedEventCount
204
+ events.removeAll(keepingCapacity: true) // drain the device buffer
205
+ sending = true
206
+ post(batch: batch, seqStart: seqStart,
207
+ viewport: (Int(w.bounds.width), Int(w.bounds.height)), attempt: 0)
208
+ }
209
+
210
+ private func post(batch: [[String: Any]], seqStart: Int, viewport: (Int, Int), attempt: Int) {
176
211
  let payload: [String: Any] = [
177
212
  "session_id": sessionId,
178
213
  "platform": platform,
179
- "viewport": ["w": Int(w.bounds.width), "h": Int(w.bounds.height)],
180
- "events": events,
214
+ "viewport": ["w": viewport.0, "h": viewport.1],
215
+ "seq_start": seqStart,
216
+ "events": batch,
181
217
  ]
182
218
  guard let body = try? JSONSerialization.data(withJSONObject: payload),
183
- let url = URL(string: "\(endpoint)/api/replay-stream/native") else { return }
219
+ let url = URL(string: "\(endpoint)/api/replay-stream/native") else {
220
+ DispatchQueue.main.async { self.sending = false }
221
+ return
222
+ }
184
223
  var req = URLRequest(url: url)
185
224
  req.httpMethod = "POST"
186
225
  req.setValue("application/json", forHTTPHeaderField: "Content-Type")
187
226
  req.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
188
227
  if let pid = projectId { req.setValue(pid, forHTTPHeaderField: "X-Project-ID") }
189
228
  req.httpBody = body
190
- URLSession.shared.dataTask(with: req).resume()
229
+
230
+ URLSession.shared.dataTask(with: req) { [weak self] _, resp, err in
231
+ guard let self = self else { return }
232
+ let ok = (resp as? HTTPURLResponse).map { (200..<300).contains($0.statusCode) } ?? false
233
+ DispatchQueue.main.async {
234
+ if ok && err == nil {
235
+ self.flushedEventCount = seqStart + batch.count
236
+ self.sending = false
237
+ } else if attempt < 3 {
238
+ let delay = pow(2.0, Double(attempt)) // 1,2,4s
239
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
240
+ self.post(batch: batch, seqStart: seqStart, viewport: viewport, attempt: attempt + 1)
241
+ }
242
+ } else {
243
+ // Give up on this attempt: re-queue at the front so the seq
244
+ // stays contiguous and the delta re-sends on the next flush.
245
+ self.events.insert(contentsOf: batch, at: 0)
246
+ self.sending = false
247
+ }
248
+ }
249
+ }.resume()
191
250
  }
192
251
  }
193
252
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sitepong",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Official SitePong SDK for error tracking and monitoring",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",