feat(DENG-11076): add onboarding_completed_by_day_27 and app_version_at_onboarding_completion to retention_clients - #9855
kbammarito wants to merge 23 commits into
Conversation
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
`rewrite_for_isolated` and `rewrite_for_defer` render query.sql to find
table refs, then write the rewritten result back. `is_init` defaults to
False, so the render collapsed `{% if is_init() %}` down to its `else`
arm and the init query was lost from the deployed copy for good.
CI's stage deploy runs `deploy --isolated`, and Test SQL executes the
staged copy: the SQL pytest plugin re-renders it with `is_init=True` for
a `test_init` directory. With the branching gone it silently got the
incremental query instead, filtered to a single `@submission_date`, so
any test_init expectation covering more than one submission_date failed.
`query initialize` on a target was affected the same way.
Render both arms, rewrite each, and re-wrap them in the conditional —
what the legacy `stage deploy` path did before it was replaced.
Surfaced by org_mozilla_fenix_derived.baseline_clients_daily_v1/test_init
on PR #9855, which stages the table via the retention_clients views. 8 of
the 9 tables with a test_init use is_init() branching, so all were
latently affected.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Integration report
|
|
@claude review |
There was a problem hiding this comment.
This adds fenix_derived.onboarding_completed_clients_v1 (one row per client per day they fired onboarding.completed, one partition per day), a passthrough view over it, and two columns on the shared retention_clients template — computed for fenix, rendered as typed NULL literals for the other five products so the positional union in telemetry.mobile_retention_clients keeps its ordinals.
Checklist notes: no existing table covers per-client onboarding completion for fenix, so the new dataset isn't duplicating available data; the one in-repo reader of fenix.retention_clients (fenix_derived/new_profile_metrics_marketing_geo_testing_v1/query.sql) names its columns, so no downstream schema needs updating; the table lands in client_id-bearing fenix_derived, which find_glean_targets picks up automatically, so the "no shredder_mitigation label" reasoning in the metadata holds. No backfill.yaml is touched.
The query's tie-breaking is well covered by the two test cases, including the NULL-version and undated-event paths. Three comments inline; the earlier note about IF(cond, TRUE, NULL) making the flag TRUE-or-NULL still stands unaddressed, and the new-column semantics comment below is closely related to it.
| type: STRING | ||
| mode: NULLABLE | ||
| description: |- | ||
| Unique ID for the client installation. Not unique in this table: a client |
There was a problem hiding this comment.
suggestion: client_id and sample_id descriptions are hand-written here and diverge from the canonical text in sql/moz-fx-data-shared-prod/fenix_derived/dataset_schema.yaml, which already defines both. docs/reference/schema_includes.md covers exactly this case, and other tables in this dataset (e.g. fenix_derived/feature_usage_events_v1/schema.yaml) already pull from that file, so these copies will drift the next time the canonical description is edited.
The table-specific caveat can ride along via append:
- name: client_id
type: STRING
mode: NULLABLE
description: !include-field-description
file: /sql/moz-fx-data-shared-prod/fenix_derived/dataset_schema.yaml
field: client_id
append: >-
Not unique in this table: a client has one row per day on which they fired the event.Same shape for sample_id. completed_date and app_version are genuinely table-specific and are fine hand-written.
| -- below stay last, in this order, and unconditional in every product branch: | ||
| -- mobile_retention_clients unions the products by position. | ||
| IF( | ||
| onboarding_completions.first_completed_date <= active_users.submission_date, |
There was a problem hiding this comment.
suggestion: The window this compares against is anchored at the row's metric_date, not at the client's first_seen_date, and first_completed_date is the client's all-time earliest completion. So on the repeat_user / existing_user rows — the majority of retention_clients — the predicate is satisfied by a completion that happened long before the row's 28-day window even opened, and the column degenerates to "ever completed onboarding". Only on rows where metric_date = first_seen_date does it mean what the name says.
Concretely: a client first seen 2025-02-01 who completed onboarding that day gets onboarding_completed_by_day_27 = TRUE on every one of their rows for the next two years, including the row whose day-27 window is 2026-08 — so a cohort cut on this column mixes "completed within 27 days of install" with "completed at some point in the past".
Anchoring on first_seen_date makes the value client-invariant and matches the name on every row:
IF(
onboarding_completions.first_completed_date
<= DATE_ADD(clients_daily.first_seen_date, INTERVAL 27 DAY),
TRUE,
NULL
)If the metric-date anchoring is deliberate, the column name and the README.md should say so explicitly ("by day 27 of the retention window", not "by day 27"), because the two readings give different numbers for every non-new-profile row.
| joining. `fenix.onboarding_completed_clients` exposes this table unchanged; | ||
| `fenix.retention_clients` takes `MIN(completed_date)` per client. | ||
|
|
||
| Coverage will begin 2025-01-01, so a client whose completions all fall before |
There was a problem hiding this comment.
nitpick: This description states the coverage floor as a fact, but the backfill is a follow-up, so once this merges the table holds only forward-fill partitions and the 2025-01-01 floor won't exist for a while. This text is what lands in the BigQuery table description and the data catalog, so a reader has no way to tell. The sibling view metadata gets this right ("Coverage will begin 2025-01-01 when the table is backfilled") — worth matching that wording here, and in the README.md "Floor" section which likewise reads as though the window is already populated.
…la#9876) * fix(deploy): preserve is_init() branching in --target rewrites `rewrite_for_isolated` and `rewrite_for_defer` render query.sql to find table refs, then write the rewritten result back. `is_init` defaults to False, so the render collapsed `{% if is_init() %}` down to its `else` arm and the init query was lost from the deployed copy for good. CI's stage deploy runs `deploy --isolated`, and Test SQL executes the staged copy: the SQL pytest plugin re-renders it with `is_init=True` for a `test_init` directory. With the branching gone it silently got the incremental query instead, filtered to a single `@submission_date`, so any test_init expectation covering more than one submission_date failed. `query initialize` on a target was affected the same way. Render both arms, rewrite each, and re-wrap them in the conditional — what the legacy `stage deploy` path did before it was replaced. Surfaced by org_mozilla_fenix_derived.baseline_clients_daily_v1/test_init on PR mozilla#9855, which stages the table via the retention_clients views. 8 of the 9 tables with a test_init use is_init() branching, so all were latently affected. * fix(deploy): pydocstyle; hoist defer target scans, widen is_init guard, add defer-path test * test: fold overlapping is_init rewrite tests into one (5 -> 4) * fix(deploy): decide is_init wrapping on rendered output, not a source substring
Description
Add
onboarding_completed_by_day_27andapp_version_at_onboarding_completiontoretention_clientsfenix_derived.onboarding_completed_clients_v1— new table, one partition per dayfenix.onboarding_completed_clients— new view over itretention_clients.view.sql— the shared template gains both columnsAll six product views gain them: fenix computes them, the other five render
CAST(NULL AS BOOLEAN)andCAST(NULL AS STRING). No existing table changes, andfenix.retentiondoesn't carry them, so no aggregate grain moves.app_version_at_onboarding_completionis the version at the completion event, not the metric-date versionretention_clientsalready carries. It is limited to the same day-27 window as the flag, so it readsNULLon exactly the rows the flag does, bar a completion event that carried no version.Grain, the floor and how to read both columns are in the table's
README.mdandschema.yaml.Consumer impact
A new column can break a downstream query two ways: a
SELECT *off the view written into a fixed-schema table, or a column landing at the wrong ordinal, sincemobile_retention_clientsunions the six product views by position and a mismatch there compiles clean and reads wrong. Both columns go last and unconditionally in all six branches, which is what holds the ordinal. Swept across both repos: every live reader names its columns, and nothing readstelemetry.mobile_retention_clientsat all. Looker, Redash and ad-hoc queries were not searched.Follow-ups
Related Tickets & Documents
Reviewer, please follow this checklist