Skip to content

Tags: lyraflow/lyraflow

Tags

v0.15.0

Toggle v0.15.0's commit message
v0.15.0 — shared dashboards

A dashboard can be shared by a secret link, minted and revoked by the operator,
that opens the dashboard with no login and lets the holder pick a preset range.
Two unauthenticated routes under /v1/shared/:token run only the tiles the
dashboard already holds, building every query from the stored report on the
server. Per link: 120 requests a minute, three runs in flight, results cached
60 seconds. The token is redacted from Lyraflow's request log.

Schema version 24: migration 024_dashboard_shares.sql adds two nullable columns
to dashboards and is additive; it applies at app start.

Limits: no embedding (and no frame header on any page, #252), no password, no
expiry, no view counts, one link per dashboard, no single-report sharing.

A release is this tag and the source at it. No container image is published;
upgrading means pulling the tag and rebuilding.

v0.14.0

Toggle v0.14.0's commit message
v0.14.0 — Accent palettes

Six alternative accents beside copper — cobalt, moss, plum, slate, wine,
amber — picked from an Appearance card on the Profile screen and kept in
the browser. Each is the copper ramp with its hue swapped and its
lightness held, measured for contrast in both modes. The accent only:
surfaces, text and the status colours do not change. A stored theme or
palette is applied before first paint, so an explicit dark choice no
longer flashes light on load. brand/tokens.css is generated, and the
ramp's tokens are named --lf-accent-* (the --lf-copper-* names are gone).
A saved report can no longer be added to a dashboard twice.

Schema version 23, unchanged since v0.13.0. No migration.

Not in this release: a palette that follows the account across browsers,
a per-project palette, custom colours, shared or public dashboards,
auto-refresh, journeys, path analysis, alerting, scheduled exports,
digests.

A release is this tag and the source at it. No container image is
published; upgrading means pulling the tag and rebuilding.

v0.13.0

Toggle v0.13.0's commit message
v0.13.0 — Dashboards

Dashboards: a named grid of tiles, each a saved trend, a saved retention
report or a funnel, with one range picker for the page (in the URL, never
stored) and a star that makes one dashboard per project the home screen.
Five routes under /v1/dashboards. Write-key rotation with a grace period,
reset-admin-login, and rejected payloads in the privacy export.

Schema version 23. Both migrations since v0.12.0 (022, 023) are additive:
two nullable columns on projects, one new table.

Not in this release: shared or public dashboards, auto-refresh, tiles that
are not saved reports, funnel breakdowns, retention splits, journeys, path
analysis, alerting, scheduled exports, digests.

A release is this tag and the source at it. No container image is
published; upgrading means pulling the tag and rebuilding.

v0.11.0

Toggle v0.11.0's commit message
Funnels ask where people stop inside one flow. Neither of the two que…

…stions after that — *do they come back at all*, and *how does this number split* — could be asked. And every condition in the product offered the same seven operators, so `url starts with https://` was not something you could write.

*Of the people who did one thing in a period, how many came back and did another in the periods after it.* `POST /v1/reports/retention`, and a **Retention** screen.

Both events are yours to choose, and `*` on either side means any event — so acquisition retention, classic same-event retention, and `signed_up → project_created` are one report rather than three.

**Each side takes a `where` clause**, the same shape a funnel step's takes, and the two are independent. Without it the report cannot ask its most ordinary question: on a site where every navigation is a `$page`, "viewed the home page, then came back and registered" is one event name and two different conditions. Measured on a demo project, the unnarrowed grid puts 261 people at 100% in period 0 — it says people who viewed a page viewed a page. Narrowed, it is 142 people at 51%, which is a conversion.

**An unfinished period is `null`, never `0`.** A cell is measured only once its period has closed; the grid shows a dash and says how many cells are waiting. A retention grid that reported unfinished periods as zero would draw a collapse in its newest cohorts, in exactly the corner a reader scans for a trend — that is the standard way this chart lies, and everything else here is built around not doing it. `computed_at` says when "not yet" was decided.

Cohorts are calendar-anchored in UTC, weeks start Monday, and a person belongs to the cohort of their **first** start event inside the range and to exactly one cohort per run. `since`/`until` bound who *enters*; the scan runs on past `until` for as long as the last cohort needs, the same entry/observation split funnels make.

A range wider than **60 cohorts** is refused rather than truncated: a grid silently missing its oldest rows is a chart with a trend that is not in the data.

*How many of this event over time, and how does that split.* `GET /v1/events/stats` — the endpoint the feed's chart already used — gains `group_by=attribute:<column>` and `group_by=property:<key>` alongside the bare `event_name` it always took, plus a `1w` interval. A **Trends** screen draws it.

Extended rather than given its own route, so the bucket cap, the `event_id` deduplication and the deletion boundary are the ones that route already enforced.

**Events with no value there are a `(not set)` series, not dropped rows**, so a split always sums to the same total the ungrouped request returns and can be checked against the feed. A property is read from **both** property bags, so a numeric one splits by its value instead of collapsing into a single empty series.

**At most ten series**, with the rest summed into a labelled `(other)` and counted in `folded_series` — ranked by total over the window, not by any single bucket, so a series does not appear and disappear as the window moves. `group_by=event_name` is **never** folded: callers read that form as the list of event names, and its cardinality is already bounded at ingest. A breakdown past 20,000 bucket/series rows is refused, not truncated.

Weeks start Monday, in UTC — the same anchoring a retention cohort uses, so a weekly trend and a weekly cohort row cannot disagree about where a week begins.

The screen draws a split as **small multiples on one shared scale** rather than overlaid coloured lines. Lyraflow's palette is a single copper ramp built for *ordinal* data like funnel stages; a breakdown's values are categorical, so a lightness ramp over them would spend the only channel there is on a rank the data does not have. Every point is marked, and hovering one reads out its bucket and value in **every** panel at once.

Four families join equality and ordering, on segment conditions and funnel-step `where` clauses alike:

| family | operators |
| --- | --- |
| text | `contains` `starts_with` `ends_with` and their negations |
| presence | `is_set` `is_not_set` |
| boolean | `is_true` `is_false` |
| relative date | `in_last` `not_in_last` |

**Which families a condition may use depends on what it compares**, and the schema enforces it rather than the editor merely hiding options: a context field or an event column takes no `is_true` (never a flag) and no `in_last` (never a date); a `lifecycle` bound takes no `is_set` (always set); a behavioural count takes comparisons only.

**`is_set` was not merely unspelled — it was unaskable.** A ClickHouse `Map` returns the value type's default for a missing key, so a property never sent and one sent as `""` read back identically and no comparison could separate them. It compiles through `mapContains` on a map and an emptiness test on a column, which are genuinely different questions.

Text matching folds both sides, so `path contains checkout` finds `/Checkout`; `=` stays case-sensitive, because changing it would reinterpret every saved segment. The folding is `lowerUTF8`, which handles accented Latin and Greek but **not** Turkish dotted `İ` — measured, not assumed, and filed as **#206**.

Negations include people who have nothing in that slot at all, which is the reading `!=` has always had here. Combine with `is_set` when you mean "has one, and it is not that".

Retention and Trends are shareable as links, with no save button, no list screen and nothing stored. Each also has a date range — presets or two dates — and each **refuses a combination the server would refuse before sending it**: 30 days at one-minute resolution is 43,200 buckets against a ceiling of 1000, and a year of daily cohorts is 365 against a ceiling of 60. Both are what you build by accident when span and resolution are two independent choices.

**No migration and no schema change.** `SCHEMA_VERSION` stays at 19.

**`AST_VERSION` stays at 1 and `FUNNEL_DEFINITION_VERSION` stays at 3.** The four operator families are additional members of a clause union and the operators already in use match the member they always did, so no saved segment or funnel is migrated or reinterpreted — pinned by a test that round-trips a tree using every previous condition kind.

`GET /v1/events/stats` is backwards compatible: `group_by=event_name` returns the `event_name` field it always did, with a `series` field added alongside.

v0.10.0

Toggle v0.10.0's commit message
v0.10.0

A release about the two screens you spend the most time on, and about a chart
that had been describing itself accurately while showing you very little.

A funnel step can be optional. Marked so, it may be skipped without
disqualifying anyone from the steps after it — conversion is measured over the
required steps alone, and the step still reports how many people did it, how
many reached that point and skipped it, and how many of those who did it
carried on. That is not a flag on an existing query: windowFunnel returns the
length of the longest matched prefix and every one of its modes makes it
stricter, so an optional step compiles to its own chain over the same rows, in
the same per-person pass.

The funnel chart is a flow diagram. Both legs out of a branch point are drawn
as flows and rejoin at the next required step, every band and node is drawn at
its own count through one scale for the whole plot, and the people who stopped
at a step leave it as a ribbon rather than as empty space the reader was
expected to infer a number from. The chart it replaced scaled each node's bands
to fill its edge exactly: the geometry always added up, and a funnel with real
drop-off drew as a solid slab.

The feed reads over a window you choose, from the last hour to the last ninety
days, with an event-name filter, both held in the URL so a refresh keeps them
and the screen can be sent as a link. Before this the chart and the table under
it had been answering questions twenty-four hours apart, silently, and the
empty state claimed a project had no events when the query had only ever looked
at one day.

No migration and no schema change. FUNNEL_DEFINITION_VERSION moves to 3, and a
build now refuses a definition a newer build wrote rather than silently dropping
the field it does not understand and reporting a smaller converted. Two limits
worth knowing before upgrading: at most two steps may be optional, measured
against ClickHouse's query-size ceiling rather than chosen, and a saved funnel
that exceeds the compiled-size guard is refused at save time — including on a
name-only rename. GET /v1/events/stats accepts event.

v0.9.0

Toggle v0.9.0's commit message
v0.9.0

A release about funnels, and about answering the question a funnel raises but
could not previously answer: who are these people.

A step can now gate on who someone is, not only on what they did — the segment
condition tree, verbatim, attached to a step and folded into that step's own
condition, so failing it stops someone advancing rather than erasing them from
the report. Clicking a step lists the people at it, reached or dropped, with
their traits. And the result itself reads as a flow, with the drop-off drawn
between the two stages it happened between and the biggest leak named in a
sentence.

No API was removed and no stored data changes meaning. FUNNEL_DEFINITION_VERSION
moves to 2, but a v1 definition still parses byte-identically — the bump exists
so a future migration can find the funnels carrying an embedded condition tree
without reading every row. /v1/funnels/:id/dropoff is unchanged and kept for
compatibility. Which is why this is a minor bump.