|
| 1 | +# Production replay acceptance: 0.4.12 → 0.4.13 |
| 2 | + |
| 3 | +Use this after shipping SDK **0.4.13** (and collector build passthrough) and |
| 4 | +running Blend through a similar flow to the baseline session. |
| 5 | + |
| 6 | +## Baseline (locked) |
| 7 | + |
| 8 | +| Field | Value | |
| 9 | +| --- | --- | |
| 10 | +| Session | `session-1785142623932166` | |
| 11 | +| SDK | `0.4.12` | |
| 12 | +| App | `to.blend.mobile_app` | |
| 13 | +| Build | `3.17.177+1472` | |
| 14 | + |
| 15 | +| Metric | Baseline | |
| 16 | +| --- | ---: | |
| 17 | +| Raw `tap` | 86 | |
| 18 | +| Swipe-consumed taps | 63 | |
| 19 | +| Settled taps | 23 | |
| 20 | +| Truly orphaned taps | 0 | |
| 21 | +| `tap_settled` `result=unknown` | 6 | |
| 22 | +| unknown `superseded_route_epoch` | 2 | |
| 23 | +| `missing_frame` with zero local/normalized | 1 | |
| 24 | +| `capture_diagnostic` `missing_context_graph_build_identity` | 37 / 37 | |
| 25 | + |
| 26 | +## Manual run checklist |
| 27 | + |
| 28 | +1. Install / point Blend at tugboat **0.4.13** (includes same-turn claims, |
| 29 | + deferred taps, session-end pointer fence, duplicate-down coalesce). |
| 30 | +2. Confirm collector with event `build` passthrough is deployed (Gate 8). |
| 31 | +3. Drive: scroll/flick on home, Get Pro / paywall, StageIt / sheet / chooser, |
| 32 | + one rapid double-tap during settle. |
| 33 | +4. Paste the new `sessionId` (+ Blend build) in chat for scoring. |
| 34 | + |
| 35 | +## Hard gates (all must PASS) |
| 36 | + |
| 37 | +1. **Identity** — `metadata.sdkVersion = '0.4.13'` on event groups. |
| 38 | +2. **No phantom taps** — `swipe_consumed_taps = 0`. |
| 39 | +3. **Settle coverage** — `settled / raw_taps >= 0.95`. |
| 40 | +4. **No same-position bursts** — no UTC second with `tap_count >= 5` and |
| 41 | + `distinct_positions = 1`. |
| 42 | +5. **Unknown settles** — `unknown / tap_settled <= 0.10` **and** |
| 43 | + `superseded_route_epoch` count = 0. |
| 44 | +6. **After frames** — 100% of `navigated`/`changed` settles have `afterFrame`. |
| 45 | +7. **Missing-frame geometry** — any `missing_frame` tap has |
| 46 | + `normalizedX/Y ∈ [0,1]` and `boundaryWidth/Height > 0`. |
| 47 | +8. **Diagnostics identity** — zero |
| 48 | + `missing_context_graph_build_identity` on `capture_diagnostic` |
| 49 | + (BLOCKED if collector not deployed). |
| 50 | +9. **Swipe start geometry** — every `swipe` has `payload.startCaptureCoordinate`. |
| 51 | + |
| 52 | +Overall verdict: **ACCEPT** only if every gate PASSes; otherwise **REJECT** or |
| 53 | +**BLOCKED** with the first failing gate id. |
| 54 | + |
| 55 | +## ClickHouse queries |
| 56 | + |
| 57 | +Replace `{newSession}` with the new session id. Service: pmkit ClickHouse. |
| 58 | + |
| 59 | +### Gate 1 — SDK version |
| 60 | + |
| 61 | +```sql |
| 62 | +SELECT |
| 63 | + argMax(metadata.sdkVersion::Nullable(String), receivedAt) AS sdkVersion, |
| 64 | + count() AS rows |
| 65 | +FROM pmkit.raw_events |
| 66 | +WHERE sessionId = {newSession} |
| 67 | +GROUP BY eventType |
| 68 | +ORDER BY eventType |
| 69 | +``` |
| 70 | + |
| 71 | +### Gates 2–3 — tap fate |
| 72 | + |
| 73 | +```sql |
| 74 | +WITH events AS ( |
| 75 | + SELECT id, |
| 76 | + argMax(eventType, receivedAt) AS eventType, |
| 77 | + argMax(metadata.relatedEventId::Nullable(String), receivedAt) AS related |
| 78 | + FROM pmkit.raw_events |
| 79 | + WHERE sessionId = {newSession} |
| 80 | + GROUP BY id |
| 81 | +), |
| 82 | +taps AS (SELECT id FROM events WHERE eventType = 'tap'), |
| 83 | +swipeRefs AS (SELECT related FROM events WHERE eventType = 'swipe' AND related IS NOT NULL), |
| 84 | +settleRefs AS (SELECT related FROM events WHERE eventType = 'tap_settled' AND related IS NOT NULL) |
| 85 | +SELECT |
| 86 | + count() AS totalTaps, |
| 87 | + countIf(id IN (SELECT related FROM swipeRefs)) AS consumedBySwipe, |
| 88 | + countIf(id IN (SELECT related FROM settleRefs)) AS settled, |
| 89 | + countIf( |
| 90 | + id NOT IN (SELECT related FROM swipeRefs) |
| 91 | + AND id NOT IN (SELECT related FROM settleRefs) |
| 92 | + ) AS orphaned |
| 93 | +FROM taps |
| 94 | +``` |
| 95 | + |
| 96 | +### Gate 4 — bursts |
| 97 | + |
| 98 | +```sql |
| 99 | +WITH taps AS ( |
| 100 | + SELECT |
| 101 | + argMax(triggeredAt, receivedAt) AS triggeredAt, |
| 102 | + argMax(payload.x::Nullable(Float64), receivedAt) AS x, |
| 103 | + argMax(payload.y::Nullable(Float64), receivedAt) AS y |
| 104 | + FROM pmkit.raw_events |
| 105 | + WHERE sessionId = {newSession} AND eventType = 'tap' |
| 106 | + GROUP BY id |
| 107 | +) |
| 108 | +SELECT |
| 109 | + toStartOfSecond(triggeredAt) AS sec, |
| 110 | + count() AS tapCount, |
| 111 | + uniqExact((round(x, 1), round(y, 1))) AS distinctPositions |
| 112 | +FROM taps |
| 113 | +GROUP BY sec |
| 114 | +HAVING tapCount >= 5 AND distinctPositions = 1 |
| 115 | +ORDER BY sec |
| 116 | +``` |
| 117 | + |
| 118 | +### Gate 5 — unknown settles |
| 119 | + |
| 120 | +```sql |
| 121 | +WITH settles AS ( |
| 122 | + SELECT |
| 123 | + argMax(result, receivedAt) AS result, |
| 124 | + argMax(toJSONString(payload), receivedAt) AS payloadJson |
| 125 | + FROM pmkit.raw_events |
| 126 | + WHERE sessionId = {newSession} AND eventType = 'tap_settled' |
| 127 | + GROUP BY id |
| 128 | +) |
| 129 | +SELECT |
| 130 | + count() AS totalSettled, |
| 131 | + countIf(result = 'unknown') AS unknownSettles, |
| 132 | + countIf( |
| 133 | + JSONExtractString(payloadJson, 'settleObservation', 'captureFailure') |
| 134 | + = 'superseded_route_epoch' |
| 135 | + ) AS supersededRouteEpoch |
| 136 | +FROM settles |
| 137 | +``` |
| 138 | + |
| 139 | +### Gate 6 — after frames on navigated/changed |
| 140 | + |
| 141 | +```sql |
| 142 | +WITH settles AS ( |
| 143 | + SELECT |
| 144 | + argMax(result, receivedAt) AS result, |
| 145 | + argMax(afterFrame, receivedAt) AS afterFrame |
| 146 | + FROM pmkit.raw_events |
| 147 | + WHERE sessionId = {newSession} AND eventType = 'tap_settled' |
| 148 | + GROUP BY id |
| 149 | +) |
| 150 | +SELECT |
| 151 | + countIf(result IN ('navigated', 'changed')) AS outcomeRows, |
| 152 | + countIf(result IN ('navigated', 'changed') AND afterFrame IS NULL) AS missingAfter |
| 153 | +FROM settles |
| 154 | +``` |
| 155 | + |
| 156 | +### Gate 7 — missing_frame geometry |
| 157 | + |
| 158 | +```sql |
| 159 | +WITH taps AS ( |
| 160 | + SELECT argMax(toJSONString(payload), receivedAt) AS payloadJson |
| 161 | + FROM pmkit.raw_events |
| 162 | + WHERE sessionId = {newSession} AND eventType = 'tap' |
| 163 | + GROUP BY id |
| 164 | +) |
| 165 | +SELECT |
| 166 | + JSONExtractString(payloadJson, 'captureCoordinate', 'unavailableReason') AS reason, |
| 167 | + JSONExtractFloat(payloadJson, 'captureCoordinate', 'normalizedX') AS nx, |
| 168 | + JSONExtractFloat(payloadJson, 'captureCoordinate', 'normalizedY') AS ny, |
| 169 | + JSONExtractFloat(payloadJson, 'captureCoordinate', 'boundaryWidth') AS bw, |
| 170 | + JSONExtractFloat(payloadJson, 'captureCoordinate', 'boundaryHeight') AS bh |
| 171 | +FROM taps |
| 172 | +WHERE JSONExtractString(payloadJson, 'captureCoordinate', 'unavailableReason') |
| 173 | + = 'missing_frame' |
| 174 | +``` |
| 175 | + |
| 176 | +### Gate 8 — diagnostics enrichment |
| 177 | + |
| 178 | +```sql |
| 179 | +WITH diags AS ( |
| 180 | + SELECT argMax(toJSONString(payload), receivedAt) AS payloadJson |
| 181 | + FROM pmkit.raw_events |
| 182 | + WHERE sessionId = {newSession} AND eventType = 'capture_diagnostic' |
| 183 | + GROUP BY id |
| 184 | +) |
| 185 | +SELECT |
| 186 | + count() AS diagnostics, |
| 187 | + countIf( |
| 188 | + JSONExtractString(payloadJson, 'contextEnrichment', 'reason') |
| 189 | + = 'missing_context_graph_build_identity' |
| 190 | + ) AS missingBuildIdentity |
| 191 | +FROM diags |
| 192 | +``` |
| 193 | + |
| 194 | +### Gate 9 — swipe startCaptureCoordinate |
| 195 | + |
| 196 | +```sql |
| 197 | +WITH swipes AS ( |
| 198 | + SELECT argMax(toJSONString(payload), receivedAt) AS payloadJson |
| 199 | + FROM pmkit.raw_events |
| 200 | + WHERE sessionId = {newSession} AND eventType = 'swipe' |
| 201 | + GROUP BY id |
| 202 | +) |
| 203 | +SELECT |
| 204 | + count() AS swipes, |
| 205 | + countIf(JSONHas(payloadJson, 'startCaptureCoordinate')) AS withStartCoord |
| 206 | +FROM swipes |
| 207 | +``` |
| 208 | + |
| 209 | +## Deliverable |
| 210 | + |
| 211 | +After scoring, write |
| 212 | +`docs/integration/production-replay-compare-0.4.12-vs-0.4.13.md` with: |
| 213 | + |
| 214 | +- session ids + SDK versions |
| 215 | +- side-by-side metric table |
| 216 | +- per-gate PASS/FAIL with deciding counts |
| 217 | +- single overall `ACCEPT` / `REJECT` / `BLOCKED` |
0 commit comments