Skip to content

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:

  1. Aggregation gap: Engine sends reachedGoal when a goal node is reached, but Logs-Parser ignores it entirely. Only reachedEnd (flow ran out of steps) is aggregated.
  2. 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

EventWhen it firesCode location
reachedGoal: trueFlow reaches a node with isGoalMiddleware.ts:48-50, MessageHolder.vue:623-626
reachedEnd: trueFlow 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 = 0 for every day from Apr 22 onward
  • During the auto-click bug period (Apr 14-21), reachedEnd DID 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

PeriodreachedEndNotes
Apr 14-20Firing (inflated)Auto-click bug: script fired on every impression
Apr 210Unclear why it stopped one day before fix
Apr 22+0Fix 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:

MetricSourceMeaning
reached_endreachedGoal OR reachedEnd OR restart (boolean)User finished at least once
completionsRestart count (+ 1 if end-of-flow)Number of full rounds
reached_goalreachedGoal event countTimes 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 reachedGoal and only counts reachedEnd, 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: restart can be used as the round counter, goal can 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: fire Analytics.sendEvent({ reachedEnd: true }) before reset
  • Future: JumpTo.ts fires same event when implemented

Relevant code paths

FileWhat
Creative-Engine/src/analytics/analytics.tssendEvent() -- sends event payloads
Creative-Engine/src/logic-system/processors/conversationFlow.tsreachedEnd: true when !this.currStep
Creative-Engine/src/components/middleware/Middleware.tsreachedGoal: true when block.isGoal
Creative-Engine/src/components/conversationflow/MessageHolder.vuereachedGoal: true when block.isGoal
Creative-Engine/src/components/blocks/functional/Restart.tsRestart operator -- no analytics event
Logs-Parser/src/Aggregator.js:31Session agg -- only MAX(reachedEnd), no reachedGoal
Logs-Parser/src/Aggregator.js:121Hourly agg -- SUM(CASE WHEN reached_end ...)
Aggregation-API/app/Validators/BucketValidator.tsreached_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).

Internal documentation