Appearance
Goal / reachedEnd Bug
Status: Open, root cause identified Discovered: 2026-04-25 during Specsavers data correction (creative 90991) Updated: 2026-05-11 with root cause analysis
Summary
"Finished Conversation" in reports is unreliable. Two separate issues:
- Aggregation gap: Engine sends
reachedGoalwhen a goal node is reached, but Logs-Parser ignores it entirely. OnlyreachedEnd(flow ran out of steps) is aggregated. - Missing event on restart: The Restart operator does not fire
reachedEnd, even though reaching a restart means the user completed a full cycle of the flow.
Root Cause (confirmed 2026-05-11)
Engine sends two separate events
| Event | When it fires | Code location |
|---|---|---|
reachedGoal: true | Flow reaches a node with isGoal | Middleware.ts:48-50, MessageHolder.vue:623-626 |
reachedEnd: true | Flow has no next step (!this.currStep) | conversationFlow.ts:91-95 |
These are independent -- a flow can reach a goal without running out of steps.
Logs-Parser only aggregates reachedEnd
In Logs-Parser/src/Aggregator.js, session aggregation (line 31):
sql
MAX(reachedEnd) AS reached_end,There is no reachedGoal column anywhere in the aggregation. The field is sent by engine, arrives in CDN logs, gets decoded, but is silently dropped during DuckDB aggregation.
Hourly summary (line 121):
sql
SUM(CASE WHEN reached_end THEN 1 ELSE 0 END) AS reached_end,This means "Finished Conversation" only counts sessions where the flow literally ran out of steps -- not sessions where a goal was reached.
Restart operator sends no analytics
Creative-Engine/src/components/blocks/functional/Restart.ts performs a full creative reset and restarts from the first node, but never calls Analytics.sendEvent(). Reaching a restart operator is a natural "finished" signal that goes untracked.
DevTools shows it correctly
The DevTools AnalyticsMonitor listens to raw postMessage events from the engine iframe, so it sees reachedGoal directly. This is why it appears to work in DevTools but not in reports.
Evidence
Creative: Specsavers // Design ditt par // Double Fullscreen // 2026 (ID: 90991)
- Goal is correctly configured on the
Go_Back?node (verified in flow editor) - Flow data confirms users reach
Go_Back?daily (1,200-1,500 impressions/day post-fix) - Report shows
Finished Conversation = 0for every day from Apr 22 onward - During the auto-click bug period (Apr 14-21),
reachedEndDID fire (55,000+ on Apr 20 alone), but this was from programmatic.click()calls on every impression
Additional testing (2026-05-11): Goal set on Choice and Wipe nodes (not Message) -- DevTools shows the event firing, but reports still show 0. Confirms the issue is in aggregation, not engine.
How this creative's flow works
Users select glasses and colors via HTML radio buttons (not flow choices). A script translates radio changes into flow navigation:
User changes radio button
-> script clicks ".c15" (Back choice node)
-> script waits for target choice to appear
-> script clicks target choice (e.g. ".c18" = Hele_Sun_RX_Blue)
-> flow progresses to Go_Back? node (goal)Each selection = 2 programmatic .click() calls on flow choice DOM elements.
Timeline
| Period | reachedEnd | Notes |
|---|---|---|
| Apr 14-20 | Firing (inflated) | Auto-click bug: script fired on every impression |
| Apr 21 | 0 | Unclear why it stopped one day before fix |
| Apr 22+ | 0 | Fix deployed, real user interactions, flow reaches goal but reachedEnd never fires |
Proposed Fix
Design
Restart is the round counter (3 restarts = 3 completed rounds). Goal is a quality signal ("user reached where we wanted them"). These are different things and should be tracked separately:
| Metric | Source | Meaning |
|---|---|---|
reached_end | reachedGoal OR reachedEnd OR restart (boolean) | User finished at least once |
completions | Restart count (+ 1 if end-of-flow) | Number of full rounds |
reached_goal | reachedGoal event count | Times user hit the goal marker |
For circular flows without a restart operator (e.g. loops via choice/wipe), reachedGoal is the only signal. This makes the goal marker useful as a design tool: place it where "finished" means something, and the number shows up in reports.
The planned JumpTo operator (see todos/jump-to-operator/plan.md) would also act as a completion trigger, same as restart.
Discord summary
I've noticed an analytics "bug" that I'd like to bring up. Finished Conversation metric has a flaw. Logs-Parser seems to drop
reachedGoaland only countsreachedEnd, which only fires when the flow runs out of steps, but in reality many flows are circular, so they never "end". I looked into possible solutions and came up with something based on this insight:restartcan be used as the round counter,goalcan be used as a quality signal. I think tracking them separately, but combining the logic could make sense for a lot of non-linear flows:
reached_end: boolean -- user finished at least once (goal OR end-of-flow OR restart)completions: count -- number of full rounds (restart count + 1 if end-of-flow)reached_goal: count -- times user hit the goal marker (separate, optional metric)
Detailed changes
Logs-Parser (src/Aggregator.js):
- Session agg: add
MAX(reachedGoal) AS reached_goal, count restart events - Hourly agg:
reached_end= sessions where any of goal/end/restart fired;completions= sum of rounds per session
Creative-Engine:
Restart.ts: fireAnalytics.sendEvent({ reachedEnd: true })before reset- Future:
JumpTo.tsfires same event when implemented
Relevant code paths
| File | What |
|---|---|
Creative-Engine/src/analytics/analytics.ts | sendEvent() -- sends event payloads |
Creative-Engine/src/logic-system/processors/conversationFlow.ts | reachedEnd: true when !this.currStep |
Creative-Engine/src/components/middleware/Middleware.ts | reachedGoal: true when block.isGoal |
Creative-Engine/src/components/conversationflow/MessageHolder.vue | reachedGoal: true when block.isGoal |
Creative-Engine/src/components/blocks/functional/Restart.ts | Restart operator -- no analytics event |
Logs-Parser/src/Aggregator.js:31 | Session agg -- only MAX(reachedEnd), no reachedGoal |
Logs-Parser/src/Aggregator.js:121 | Hourly agg -- SUM(CASE WHEN reached_end ...) |
Aggregation-API/app/Validators/BucketValidator.ts | reached_end in ALL_METRICS |
Workaround
For reporting, use flow node impressions (from daily flow data) as a proxy for "Finished Conversation". This was used in the Specsavers corrected report (SpecsaversDataCorrection/specsavers-design-ditt-par-rapport.xlsx).