Skip to content

Commit 1f272b7

Browse files
authored
Merge pull request #28 from blendto/fix/replay-interaction-transactions
feat(replay): consolidate interaction evidence
2 parents 427f3ef + cdf0027 commit 1f272b7

37 files changed

Lines changed: 3166 additions & 413 deletions

docs/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ verified in their own repositories.
2828

2929
## Current compatibility
3030

31-
- package version: `0.4.12`;
32-
- session JSON schema: `7`;
31+
- package version: `0.4.15`;
32+
- session JSON schema: `8`;
3333
- fingerprint schema: `6`;
3434
- minimum Dart SDK: `3.9.2`;
3535
- minimum Flutter SDK: `3.35.0`.
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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`
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Production replay acceptance: interaction consolidation (0.4.15)
2+
3+
Use this after shipping SDK **0.4.15** (canonical interactions + delayed
4+
reconciliation) and running Blend through the acceptance flow.
5+
6+
## Baseline (locked)
7+
8+
Prefer the nearest prior Blend session against SDK **0.4.12 / 0.4.13** for
9+
side-by-side scoring. Record the new session id and Blend build before scoring.
10+
11+
## What changed in the SDK
12+
13+
| Concern | 0.4.13 behavior | 0.4.15 behavior |
14+
| --- | --- | --- |
15+
| Gesture identity | `tap` + `tap_settled` peers | one `interaction` (`stream: semantic`) + legacy projection |
16+
| Claim window | microtask same-turn only | default 1,250 ms delayed reconciliation |
17+
| Diagnostics | mixed into normal events | `stream: diagnostic` |
18+
| Origin | frozen on pending tap | immutable `InteractionOrigin` on the transaction |
19+
| Swipe state | refreshed at pointer-up | frozen to pointer-down origin |
20+
21+
## Manual run checklist
22+
23+
1. Point Blend at tugboat **0.4.15**.
24+
2. Drive: home scroll/flick, Get Pro / paywall, full-screen navigation, modal
25+
bottom sheet, asynchronous onboarding transition, rapid double-tap,
26+
automatic redirect after a settled tap.
27+
3. Paste the new `sessionId` (+ Blend build) for scoring.
28+
29+
## Hard gates (all must PASS)
30+
31+
1. **Identity**`metadata.sdkVersion = '0.4.15'`.
32+
2. **Canonical coverage** — one `stream: semantic` `interaction` per completed
33+
user gesture (tap / swipe / scroll / cancelled).
34+
3. **Origin correctness** — interaction `origin.route` / `origin.targetAnchor`
35+
match the pointer-down screen/component, never the destination.
36+
4. **Delayed attribution** — delayed navigation / bottom sheet inside 1,250 ms
37+
has `attribution.kind = delayed_likely` (or `direct`) and
38+
`result.status = navigated|changed`, with matching
39+
`route_change.causedByInteractionId`.
40+
5. **Automatic false-claim rate** — timer/auth redirects after the window, and
41+
routes with competing pointers, stay `navigationOrigin =
42+
automatic_or_unknown`.
43+
6. **No semantic tap for scrolls/swipes** — completed scroll/swipe produces no
44+
`stream: semantic` tap; one `interaction` with `gesture=scroll|swipe`.
45+
7. **Diagnostic isolation** — enrichment selection of `stream: semantic`
46+
excludes `capture_diagnostic`.
47+
8. **Rage-tap precision** — three no-result taps on the same origin target flag
48+
once; three scrolls or three successful navigation taps do not.
49+
50+
## Soft / observational
51+
52+
- Semantic event count per completed gesture should drop vs 0.4.0 raw
53+
`tap`+`tap_settled`+scroll peer inflation.
54+
- Legacy projection remains present until collector/graph cut over; do not
55+
delete `tap`/`tap_settled` selection until two representative Blend flows pass
56+
on canonical interactions alone.
57+
- Instrument pending-to-success latency; retune `interactionClaimWindow` from
58+
production evidence if 1,250 ms is too short/long.
59+
60+
## Consumer follow-ups (separate PRs)
61+
62+
- Collector / Context Graph enrichment select `stream: semantic` `interaction`
63+
and map components via `origin.targetAnchor`.
64+
- Build causal edges from `result` / `causedByInteractionId`.
65+
- Update dashboard rage-tap detectors to the definition above.

docs/integration/production-replay-acceptance.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,19 @@ database receipt alone as proof that a replay is correct.
1212

1313
## Current acceptance status
1414

15-
Production acceptance #13/#14 remains open. The SDK's route-epoch and frame
16-
provenance behavior is an intended invariant, but rapid/nested modal chains and
17-
programmatic/automatic navigation can still be absent or degraded in a
18-
production replay. Record those observations as SDK capture gaps; do not infer
19-
route/action coherence from the intended contract or repair the evidence in the
20-
dashboard. Stored tap coordinates are global logical pixels and are not
21-
capture-boundary-normalized for playback, so fractional overlay drift is also a
22-
known limitation.
15+
Interaction consolidation shipped in SDK **0.4.15** (canonical `interaction`
16+
events, 1,250 ms delayed claim window, diagnostic stream isolation). Use
17+
[`production-replay-acceptance-0.4.15.md`](./production-replay-acceptance-0.4.15.md)
18+
for the Blend scoring gates. Collector/Context Graph migration onto
19+
`stream: semantic` interactions remains a follow-up before legacy
20+
`tap`/`tap_settled` projection can be removed.
21+
22+
Production acceptance #13/#14 remains open for rapid/nested modal chains and
23+
programmatic/automatic navigation gaps. Record those observations as SDK
24+
capture gaps; do not infer route/action coherence from the intended contract or
25+
repair the evidence in the dashboard. Stored tap coordinates are global logical
26+
pixels and are not capture-boundary-normalized for playback, so fractional
27+
overlay drift is also a known limitation.
2328

2429
## Roles and evidence
2530

0 commit comments

Comments
 (0)