Module kit / Analytics and tracking
Tracking Settings and Site Registration
analytics-tracking/tracking-settings-and-site-registration
| Kind | backend |
|---|---|
| Domain | analytics-tracking |
| Maturity | prod |
| Reusable | extract |
| Stack | fastapi-0.118, sqlalchemy-2.0-sync, postgres, pydantic-2.12 |
| Depends on | _shared/fastapi-project-layout, _shared/workspace-scoping, _shared/auth-dependency-pattern, _shared/alembic-migrations |
| Frontends | analytics-tracking/ui/tracking-dashboard |
| Reference implementation |
|
| Measured on | alchemy_labs main 62ba4fc41009b454e1c033e5036c7e8577db2061 2026-09-12 |
| Version | 1 |
Purpose
A workspace registers which public site_id values may post tracking events to it, and holds, per workspace, a
master enabled/test-mode switch, per-event-type per-platform send toggles, and two conversion-mapping tables that
translate a form or a calendar meeting-type into the named event each ad platform expects. The invariant that
makes the registry half trustworthy is enforced at the schema, not just the handler: site_id is the table's own
primary key (models/tracking_model.py 39), so a site_id can belong to at most one workspace ever, which is what
lets ingestion resolve an inbound X-Site-ID to exactly one workspace without a second lookup
(tracking_sites_router.py module docstring, 1 to 8). The settings half has no equivalent hard guarantee: it is a
handful of plain dict keys inside one JSON column shared with ad-platform credentials this recipe does not own (see
Data model), and its only trustworthiness property, that a broken or missing row degrades to typed defaults rather
than a 500, is a caught exception, not a schema constraint (get_workspace_tracking_settings, 75 to 90).
When to use
Read it before touching TrackingSite CRUD (list_sites/create_site/update_site/delete_site,
tracking_sites_router.py), before touching get_tracking_config's enabled/test_mode/event-toggle
computation or either conversion-mapping getter (tracking_settings_service.py), and when someone asks "why did
site_id X get rejected", "is tracking on for this workspace", "why is test_mode logging instead of sending", "what
does this form/this meeting-type fire as a conversion on Meta/LinkedIn/Google", or "why does /api/track/config 404".
Trigger phrases: "tracking site registration", "site_id registry", "master enabled flag", "test_mode", "event
toggle", "send_to_meta", "send_to_linkedin", "send_to_google", "form conversion mapping", "calendar conversion
mapping", "conversion_id_lead", "conversion_id_high_intent".
Not this recipe: the per-platform credential fields inside the very same tracking dict (meta_pixel_id,
meta_access_token, linkedin_access_token, linkedin_ad_account_id, google_ads_customer_id,
google_ads_refresh_token, google_ads_client_id/_secret, and the rest of MetaTrackingConfig/
LinkedInTrackingConfig/GoogleAdsTrackingConfig's own fields), their Fernet encryption, and the OAuth
connect/refresh flows that populate them, all owned by ads/ad-platform-connections as the typed reader of the
same settings dict; the generic PATCH /api/workspaces/{workspace_id}/settings endpoint that is this repo's only
write path for the keys this recipe documents (api/routers/workspace_router.py 606 to 641, services/workspace /tenant_service.py update_workspace_settings, outside this recipe's three assigned files, cited for grounding
only, see Gotchas); the ingest pipeline that actually checks an inbound X-Site-ID against the registry
(services/tracking_service.py, referenced in tracking_sites_router.py's own module docstring but not one of
the three assigned files); the outbound CAPI senders that consume TrackingConfig/the conversion mappings to make
the actual platform call (services/meta_capi_service.py, services/linkedin_capi_service.py,
services/google_ads_service.py, all imported by tracking_router.py but not this recipe's subject); the
read-scoping of tracking DATA (sessions, events, contacts) by site - require_site_id_or_admin,
_session_visible_to, _contact_visible_to, _workspace_ids_for_site - a neighbouring concern in the same file
(tracking_router.py) and this recipe's own assigned test file's actual subject, cited here because of that
overlap but not walked as this recipe's own logic (see Tests).
Stack assumptions
At 62ba4fc: fastapi==0.118.2, synchronous handlers on a sqlalchemy.orm.Session (SQLAlchemy==2.0.43),
pydantic==2.12.0 for TrackingSiteResponse/TrackingSiteCreate/TrackingSiteUpdate, psycopg2-binary==2.9.10.
Foundations assumed: S4 (fastapi-project-layout) for mount_router, which both routers in scope go through
(app_fastapi.py 956, 960); S5 (auth-dependency-pattern) for ActiveUser
(Annotated[User, Depends(get_current_active_user)]) on every tracking_sites_router.py route, and for the plain
get_current_user dependency GET /config uses instead (see Security & tenancy); S6 (workspace-scoping) for
WorkspaceID (Annotated[UUID, Depends(_get_workspace_id)]), which every route in scope takes to resolve and
access-check the caller's workspace (api/dependencies.py 680 to 742); S8 (alembic-migrations) for the one
migration under Deploy & ops.
Environment: none of this recipe's own settings keys (enabled, test_mode, events, form_conversion_mappings,
calendar_conversion_mappings) has an environment-variable name anywhere in the three assigned files - unlike the
adjacent credential fields in the same dict, which get_tracking_config does fall back to an env var for (owned by
ads/ad-platform-connections, cited only for contrast). Fail behaviour: get_workspace_tracking_settings never
raises, it catches every exception and logs a warning, returning None (75 to 90); every reader built on it degrades
to a typed default rather than propagating a 500 - tracking_enabled defaults True (110 to 112), test_mode
defaults False (172), both mapping getters return None (271 to 273, 321 to 323) when there is no workspace
tracking config at all, or when the underlying decrypt or DB read failed. get_workspace_tracking_settings itself
calls decrypt_tracking_tokens (services/security/oauth_token_crypto.py, 84 to 85) on every read, decrypting the
WHOLE tracking dict even though none of the keys this recipe owns is ever Fernet-encrypted (see Security &
tenancy) - that decrypt step and the key it depends on belong to ads/ad-platform-connections's Data model, cited
here only because every read this recipe makes goes through it.
Data model
Two structurally different storage shapes for the two halves of this recipe's scope.
The registry, its own table, created by the migration under Deploy & ops (models/tracking_model.py 29 to 52):
tracking_sites (
site_id varchar PRIMARY KEY, -- the ingest-trusted key itself, not a surrogate id
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
name varchar NOT NULL,
allowed_origins jsonb DEFAULT '[]', -- CSP-style allowlist; empty = not enforced
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
)
INDEX ix_tracking_sites_workspace (workspace_id)
Everything else lives inside workspace_settings.settings['tracking'], one JSON (not JSONB) column shared
with ads/ad-platform-connections's OAuth tokens (table workspace_settings, services/workspace/models.py 82 to
96, cited in that recipe's own Data model, same column). The keys this recipe owns, reconstructed from every place
tracking_settings_service.py reads them, no schema file defines the shape, and grounded by the default-settings
seed written at workspace creation (services/workspace/tenant_service.py _get_default_settings, 405 to 462,
outside this recipe's three assigned files, cited for grounding since no assigned file defines a default):
{
"enabled": true, // master switch; AND-ed into every platform's own `enabled` in TrackingConfig
"test_mode": false, // true: CAPI calls are logged, never POSTed to a provider (tracking_settings_service.py 171-172)
"events": {
"form_submission": { "enabled": true, "send_to_meta": true, "send_to_linkedin": true, "send_to_google": true,
"meta_event_name": "Lead", "google_event_name": "generate_lead" },
"page_view": { "enabled": true, "send_to_meta": true, "send_to_linkedin": false, "send_to_google": true },
"button_click": { "enabled": false, "send_to_meta": false, "send_to_linkedin": false, "send_to_google": false }
},
"form_conversion_mappings": [
{ "form_id": "example-form-id", "form_name": "Example contact form",
"meta_event": "Lead", "linkedin_event": "Lead", "google_event": "generate_lead", "enabled": true }
],
"calendar_conversion_mappings": [
{ "slug": "strategy-call", "meeting_name": "Strategy Call",
"meta_event": "Lead", "linkedin_event": "Lead", "google_event": "BOOK_APPOINTMENT", "enabled": true }
]
}
The two mapping arrays' shapes are fully determined by their own getters and, for the calendar one, by the
function's own worked example in its docstring (tracking_settings_service.py 313 to 319). The default-settings
seed above shows the three seeded events entries but seeds neither mapping array, so a freshly-created workspace
starts with both empty.
What is NOT stored anywhere in the three assigned files: any per-platform token, pixel id, customer id, developer
token or ad account id (meta_pixel_id, meta_access_token, linkedin_access_token, google_ads_refresh_token,
etc.), real, adjacent keys in the same tracking dict, all owned and typed by ads/ad-platform-connections; a
stable id for a row inside either conversion-mapping array, an entry is addressed only by its own form_id or
slug, matched with a linear scan (get_form_conversion_mapping 280 to 289, get_calendar_conversion_mapping 329
to 338), first match wins, nothing rejects a stored duplicate; any FK from a mapping entry to an actual form
template or calendar meeting-type row, form_id/slug are free strings, never validated against another table
from within these three files.
API surface
| Route | Auth | Tenancy | Body | Codes | Notes |
|---|---|---|---|---|---|
GET /api/tracking-sites (tracking_sites_router.py 50 to 59) | ActiveUser | WorkspaceID | - | 200 List[TrackingSiteResponse] | WHERE workspace_id = :workspace_id ORDER BY created_at DESC, no pagination |
POST /api/tracking-sites (62 to 86) | ActiveUser | WorkspaceID | TrackingSiteCreate {site_id (1-255 chars), name (1-255 chars), allowed_origins?, is_active?} | 201 TrackingSiteResponse; 409 if site_id already exists | the existence check (70 to 71) queries site_id alone, not scoped to the caller's workspace, see Gotchas |
PATCH /api/tracking-sites/{site_id} (89 to 116) | ActiveUser | WorkspaceID | TrackingSiteUpdate {name?, allowed_origins?, is_active?}, all optional | 200 TrackingSiteResponse; 404 if no row matches site_id AND workspace_id | partial update, only fields that are not None are applied (108 to 113) |
DELETE /api/tracking-sites/{site_id} (119 to 143) | ActiveUser | WorkspaceID | - | 204; 404 if no row matches site_id AND workspace_id | deletes the registry row only; existing tracking_sessions/tracking_events keep their workspace_id (handler's own docstring, 126 to 129); future events for that site_id are simply no longer accepted at ingest, a check outside this recipe |
GET /api/scribe/config (tracking_router.py 270 to 314) | Depends(get_current_user) (not ActiveUser) | WorkspaceID | - | 200 {success, site_id, has_project, tracking_enabled, meta_pixel_id?, google_ads_id?, google_ads_label?, linkedin_partner_id?, bing_uet_tag_id?} (the last five present only if truthy, 313) | see notes below |
GET /config sits on tracking_router.router, mounted surface_exempt("public", "Scribe beacon - no auth by design") (app_fastapi.py 956) - the surface gate itself imposes no credential requirement on this router - but
the handler's own Depends(get_current_user) still 401s with no credential, making this the one authenticated
route on an otherwise-public router; it does not get the is_active check ActiveUser would add. site_id in
this response is NOT read from tracking_sites, it comes from projects.tracking_site_id, minted
(f"site_{str(project.id)[:8]}") on first call if the user has a Project row and none is set yet (287 to 290), a
second, older site-id mechanism that predates and does not consult the registry this recipe otherwise documents.
The five extra pixel-id fields belong to ads/ad-platform-connections's Data model, surfaced here only for the
client-side install snippet, wrapped in a bare try/except Exception that logs at debug and silently omits them
on any failure (292 to 306). The legacy alias router (legacy_router, prefix /api/track) does NOT re-declare this
route, see Gotchas.
Application logic
Site registration (tracking_sites_router.py, commit order):
create_site(62 to 86): existence check queriesTrackingSitebysite_idalone (70), independent of workspace, raising 409 before any insert; else buildsTrackingSite(site_id, workspace_id=<caller's, from the header>, name, allowed_origins or [], is_active), adds, commits, refreshes, returns 201. No advisory lock or retry around the check-then-insert; a concurrent duplicatePOSTwould surface as anIntegrityErrorat commit rather than this handler's own 409, uncaught here.update_site(89 to 116): loads the row scoped to BOTHsite_idand the caller'sworkspace_id(100 to 103), unlikecreate_site's existence check, so a caller cannot even learn whether asite_idit doesn't own exists (404, not 409, for someone else's site); applies only the payload fields that are notNone; commits.delete_site(119 to 143): the same double-scoped lookup as update; deletes the row outright, no soft-delete path exists even though the model already has anis_activecolumn that could serve one; does not touchtracking_sessions/tracking_events(handler's own docstring, 126 to 129).
get_tracking_config's enabled/test_mode/event-toggle computation (tracking_settings_service.py 93 to 180),
in the order the function runs it:
ws_tracking = get_workspace_tracking_settings(db, workspace_id)only if aworkspace_idwas passed at all (105 to 107); called with none, every downstream default applies as if no workspace config existed.tracking_enabled = ws_tracking.get('enabled', True)(110 to 112), the master switch, defaulting on.- Each platform's own
enabledistracking_enabled AND _get_<platform>_enabled(ws_tracking)(116, 129, 143): the master switch is a hard AND-gate no per-platform function can override, but_get_meta_enabled/_get_linkedin_enabled/_get_google_ads_enabled(190 to 232) each returnTruefrom EITHER a stored credential (meta_pixel_id,linkedin_access_token,google_ads_customer_id/google_ads_id, owned byads/ad-platform-connections) OR this recipe's ownevents.form_submission.send_to_<platform>toggle, which defaultsTruewhen absent (199, 214, 229). A platform with a toggle object but zero credentials therefore still readsenabled: true, purely off that default; see Gotchas. test_mode = bool(ws_tracking.get('test_mode')) if ws_tracking else False(171 to 172), the one field in this recipe's scope with no per-platform variant and no env-var fallback at all.
The two conversion-mapping getters (255 to 291, 305 to 340): each calls get_workspace_tracking_settings
itself (271, 321) rather than accepting an already-loaded dict, so resolving both mappings for one request costs
two separate DB reads and two separate whole-dict decrypts of data neither function needs decrypted; each then
scans its own array (form_conversion_mappings / calendar_conversion_mappings) comparing mapping.get('form_id') == str(form_id) or mapping.get('slug') == slug, returning the first hit, None if the array is empty or absent.
Invariants
- A
site_idbelongs to at most one workspace, ever: enforced by the column's ownPRIMARY KEY(models/tracking_model.py39, migrationtracking_sites_001), withcreate_siteadding a friendlier 409 in front of it on the happy path (70 to 75). This recipe's one assigned test file exercises neither the 409 path nor a concurrent-insert race; the guarantee is a schema-level fact here, not a tested one. - A caller can only read or modify a
tracking_sitesrow it owns:update_site/delete_sitescope their lookup onworkspace_idas well assite_id(100 to 103, 133 to 136). The one asymmetry iscreate_site's existence check, which reveals, as a 409 with no row contents, whether asite_idis taken anywhere on the estate, before workspace ownership is even considered (see Gotchas). get_workspace_tracking_settingsnever raises regardless of what is wrong with the row, the JSON, or the decrypt step beneath it (75 to 90); every function in this recipe built on it (get_tracking_config, both mapping getters) degrades to its own typed default rather than a 500 when workspace tracking config is missing or malformed.- The property this recipe's own assigned test file actually proves ("only an admin is ever unrestricted" over
which
site_ids a request may read,test_tracking_scope.py's own module docstring) belongs torequire_site_id_or_admin, a read-scoping function for tracking DATA in the same file, not to the site REGISTRY or the SETTINGS this recipe documents. It is cited here because it is this recipe's one assigned test source, not because it tests registration or the config toggles; see Tests.
Frontend
The module that would render this, by id:
analytics-tracking/ui/tracking-dashboard the workspace-level surface a person uses to see which site_ids are
registered (name, allowed_origins, active/inactive), register a new
one, and set the master enabled/test_mode switches, the per-event
platform toggles, and the two conversion-mapping tables; at 62ba4fc
nothing in this recipe's three assigned files renders any of this, and
the write path for everything except the site registry itself is the
generic PATCH /api/workspaces/{id}/settings (outside this recipe's
scope, see Gotchas), so a real dashboard calls this recipe's CRUD and
read routes plus that generic settings endpoint, not this recipe alone
Minimum admin/debug surface: none of the three assigned files exposes a dedicated admin/debug route or a scheduled
log line (contrast the auto-topup and ads recipes, both of which have one or the other). The closest a human has:
SELECT site_id, workspace_id, name, is_active FROM tracking_sites ORDER BY created_at DESC (unscoped version of
what list_sites runs, useful for spotting a global site_id collision before a POST would hit the 409); reading
the settings half at the Python layer only, e.g. (get_workspace_tracking_settings(db, workspace_id) or {}).get('events'), for the same reason given in the ad-platform-connections recipe: workspace_settings.settings
is JSON, not JSONB, so no ->>/? operator is available for a plain SQL check.
Security & tenancy
Auth dependency: tracking_sites_router.router is mounted under LABS_SURFACES (app_fastapi.py 960), and all
four of its routes take ActiveUser (_shared/auth-dependency-pattern), 401 on no or invalid credential, 403 on
is_active=False (api/dependencies.py 344 to 348). tracking_router.router, carrying GET /config, is mounted
surface_exempt("public", "Scribe beacon - no auth by design") (app_fastapi.py 956); the surface gate itself
imposes nothing, but /config's own Depends(get_current_user) still 401s on no credential, without the
is_active check ActiveUser would add (grounded by comparing the two routers' dependency signatures, both read
directly).
Tenancy: WorkspaceID (_shared/workspace-scoping) resolves and access-checks the workspace on every route in
scope in priority order - pre-validated request.state.workspace_id, a re-validated X-Workspace-ID header, or
single-workspace inference (api/dependencies.py 680 to 742) - 400 on an invalid header, 403 if the resolved user
lacks access to the requested workspace, 400/409 if no workspace can be inferred at all. create_site's existence
check is the one place in scope where a caller learns something about a site_id before its own workspace access
to that site is even considered; see Application logic and Gotchas.
Fail direction: get_workspace_tracking_settings fails toward the safest READ outcome (None, on any error, 88 to
90), but its CONSUMERS do not all fail toward the safer state: get_tracking_config reads tracking_enabled = True
when there is no workspace config at all (110 to 112), so a workspace with a missing or broken settings row is, by
default, MORE permissive about tracking than one that explicitly configured it, not less; test_mode defaults
False under the same condition (172), so a broken settings row also fails toward "actually send to providers"
rather than the safer "log only" state. Both are dated facts about this commit, not a claim about what should
happen.
Secrets and PII at rest: none in the registry table (tracking_sites has no secrets column). The settings keys this
recipe owns (enabled, test_mode, events, both mapping arrays) are never Fernet-encrypted, SCALAR_TOKEN_FIELDS
(cited in the ad-platform-connections recipe) lists only credential fields, none of this recipe's own keys, but
every read through get_workspace_tracking_settings decrypts the whole tracking dict regardless (84 to 85), so
this recipe's own functions pay the decrypt cost for data they neither own nor ever see as ciphertext. A mapping
entry's form_name/meeting_name is operator-chosen free text, unvalidated and untruncated by any of the three
assigned files. Rate limits: none dedicated to any of the five routes/entry points in scope.
Attacker-controlled inputs: site_id on POST /api/tracking-sites (TrackingSiteCreate.site_id, 1 to 255 chars,
no charset or slug-format restriction) is exactly the string ingest later trusts as X-Site-ID, per the router's
own module docstring (1 to 8); allowed_origins is an unvalidated List[str] stored as-is into jsonb, its
enforcement as a CSP-style allowlist happens at ingest, outside this recipe; name on create and update is 1 to
255 chars, no other constraint.
Tests
Run 2026-09-12 from alchemy_labs/backend at 62ba4fc, using the venv at alchemy_labs/backend/.venv, no database
and no network:
.venv/bin/python -m pytest tests/test_tracking_scope.py -q --no-cov -o addopts="" -p no:cacheprovider
prints 14 passed (0 failed; 162 warnings, all unrelated Pydantic-deprecation/regex noise from importing
tracking_router.py's other schemas). grep -c "^def test_" tests/test_tracking_scope.py prints 12;
grep -c "@pytest.mark.parametrize" tests/test_tracking_scope.py prints 1
(test_non_admin_is_never_unrestricted, 3 cases, the parametrize that takes 12 defs to 14 passed).
Hermetic vs live: fully hermetic, no database, every db argument is a unittest.mock.MagicMock whose
.execute().all() is preset to a list of (site_id,) tuples (_db_with_sites, 44 to 47); every user is a
MagicMock(is_admin=...) (_user, 50 to 53). Known-answer vectors, all against require_site_id_or_admin,
_session_visible_to, and the two cache-key helpers: an admin with no requested site gets None (unrestricted,
58 to 59); an admin requesting one site gets exactly [that site] (62 to 65); a non-admin is NEVER unrestricted
across zero, one, or three reachable sites, parametrized (67 to 72); a non-admin sees every site across their
workspaces, proven with four Dein Team domains as the fixture data (77 to 82); a non-admin may narrow to one of
their own sites (85 to 90) but requesting someone else's site is ignored, not honoured, their own set is returned
unchanged (93 to 99); a non-admin with no reachable sites gets [NO_TRACKING], the sentinel that matches no row
(102 to 104); the cache-key helper is order-independent (_k_sites(['b','a']) == _k_sites(['a','b']), 123 to 126)
and distinguishes an admin's 'all' from any real scope (129 to 132).
What this recipe's own subject matter has NO test for, anywhere in the repo, not only outside this one file:
grep -rl "FormConversionMapping\|CalendarConversionMapping" tests/ (run from alchemy_labs/backend) returns no
file, confirming neither dataclass, nor the matching logic inside get_form_conversion_mapping/
get_calendar_conversion_mapping, is exercised anywhere; both getters DO appear elsewhere in the test tree
(tests/v2/coverage/test_booking_g6_...py) but only as patch.object(bc, "get_calendar_conversion_mapping", return_value=...), a monkeypatched return value standing in for this recipe's function, never a test of the
function's own array-scanning behaviour. TrackingSite similarly appears in other test files
(tests/integration/test_tracking_multi_tenant.py, tests/test_scribe_query.py) only as a fixture-construction
helper for testing the read-scoping concern named in Application logic's last invariant, never as a test of
tracking_sites_router.py's own CRUD handlers, their 409/404 behaviour, or the tenancy asymmetry in Gotchas. A
rebuild of this recipe has no existing test suite to port for its own registration or settings-computation logic,
only for the neighbouring scoping concern this recipe's one assigned test file actually covers.
Deploy & ops
Config keys: none. This recipe's own settings (enabled, test_mode, events, both mapping arrays) have no
environment-variable name anywhere in the three assigned files, unlike the credential fields elsewhere in the same
tracking dict. Migrations: one in scope, tracking_sites_001
(migrations/versions/20260504c_tracking_sites_registry.py, down-revision workspace_id_2b_001): backfills
projects.tracking_site_id if the column is missing (add_column_if_missing, an idempotent-add helper, not a bare
Alembic op, 53 to 54, because the column was historically applied by hand outside Alembic's own tracking); creates
tracking_sites and its ix_tracking_sites_workspace index (56 to 71); seeds rows from three sources in order -
every active workspace self-mapped (site_id = workspace.id::text), every project's existing tracking_site_id
resolved through its owner's single workspace, and a hardcoded operator-supplied domain-to-workspace map (real
client domains and workspace ids in the migration's own source, elided here per house rules) - all three inserts
are ON CONFLICT (site_id) DO NOTHING (76 to 126), so the migration is safe to re-run. downgrade() drops the
index and the table (129 to 131), it does not undo the projects column backfill. No migration in scope touches
the settings-dict keys this recipe documents; they live inside workspace_settings.settings, a column that already
existed before this recipe's scope, seeded only at workspace-creation time (TenantService._get_default_settings,
outside this recipe's three assigned files, cited for grounding).
Scheduler: none. Argus-style checks: none exist in the three assigned files; the natural one, given the grounding
cited throughout (GET /api/scribe/config with a valid session and X-Workspace-ID returns 200 with
success: true), is a recommendation here, not a measured fact. Rollback: tracking_sites_001's downgrade()
works as described above; a tracking_sites row is an ordinary, deletable row with no other table's FK depending
on it (tracking_sessions/tracking_events do not reference it, per delete_site's own docstring), so a rollback
of the code carries no data-loss risk visible from these three files.
Gotchas
- 2026-09-12 (measured at 62ba4fc):
legacy_router(tracking_router.py263, mounted atapp_fastapi.py957 under the/api/trackprefix with the tagscribe-legacy) carries zero routes.grep -n "legacy_router\." api/routers/tracking_router.py(the only place it could be decorated) returns no match anywhere in the file, and nothing elsewhere in the repo imports it either.GET /api/track/configdoes not exist despite the "Legacy" naming implying a full alias of the/api/scriberouter; onlyGET /api/scribe/configis real. - 2026-09-12 (measured at 62ba4fc):
POST /api/tracking-sites's existence check (70 to 71) queriessite_idalone, with noworkspace_idfilter, so any authenticated caller learns, via a 409 with no row contents, whether a givensite_idis already registered anywhere on the estate, before any workspace-ownership check runs. Not a data leak on its own, but a cross-tenant existence oracle on a value operators may pick predictably (a domain name, a short slug) - the one asymmetry withupdate_site/delete_site, which scope their lookup to the caller's own workspace from the start. - 2026-09-12 (measured at 62ba4fc):
PATCH /api/workspaces/{workspace_id}/settings(api/routers/workspace_router.py606 to 641, outside this recipe's three assigned files, cited for grounding) is the only write path this repo has forenabled,test_mode,events, and both conversion-mapping arrays, andTenantService.update_workspace_settings(services/workspace/tenant_service.py329 to 355) REPLACESworkspace_settings.settingsoutright (workspace_settings.settings = settings, line 350) rather than merging it. A client that PATCHes only{"settings": {"tracking": {"enabled": false}}}therefore overwrites every other top-level settings key AND every othertrackingsub-key in the same request, including every ad-platform credentialads/ad-platform-connectionsowns, not just the field this recipe's caller meant to change. Neither of this recipe's three assigned files performs or guards against this write; it is named here because it is the single biggest operational risk to the data this recipe documents, and nothing in scope owns a fix for it. - 2026-09-12 (measured at 62ba4fc):
get_form_conversion_mappingandget_calendar_conversion_mappingeach callget_workspace_tracking_settingsthemselves (271, 321) rather than accepting an already-loaded dict; resolving both mappings for one event (a booking page that is also a form, say) costs two separate DB reads and two separate whole-tracking-dict decrypts, for data neither function needs decrypted at all. - 2026-09-12 (measured at 62ba4fc):
_get_meta_enabled/_get_linkedin_enabled/_get_google_ads_enabled(190 to 232) default everysend_to_<platform>event toggle toTruewhen the key is simply absent, so a workspace with a storedeventsobject but NO platform credentials configured at all still reads that platform'senabledasTruefromget_tracking_config, purely off the toggle default. Whether this is intended (a workspace usingtest_modelogging before ever connecting a platform) or an oversight in the default is not determinable from this recipe's three assigned files alone.
Reference implementation
Paths relative to the repo root; all at main 62ba4fc41009b454e1c033e5036c7e8577db2061 unless noted.
Primary (the three assigned source files plus the one assigned test file):
alchemy_labs/backend/services/tracking_settings_service.py(340 lines, whole file in scope): module docstring (1 to 6); theMetaTrackingConfig/LinkedInTrackingConfig/GoogleAdsTrackingConfig/TrackingConfigdataclasses (20 to 61, fields outside this recipe's ownenabled/test_modecited only for shape, owned byads/ad-platform-connections);get_workspace_tracking_settings(64 to 90);get_tracking_config(93 to 180);_get_setting/_get_meta_enabled/_get_linkedin_enabled/_get_google_ads_enabled/_get_event_setting(183 to 241);FormConversionMapping/get_form_conversion_mapping(244 to 291);CalendarConversionMapping/get_calendar_conversion_mapping(294 to 340).alchemy_labs/backend/api/routers/tracking_sites_router.py(143 lines, whole file in scope): module docstring (1 to 8); the three Pydantic schemas (24 to 47);list_sites(50 to 59);create_site(62 to 86);update_site(89 to 116);delete_site(119 to 143).alchemy_labs/backend/api/routers/tracking_router.py:GET /config(270 to 314) is this recipe's own subject;NO_TRACKING/get_user_site_ids/require_site_id_or_admin/_session_visible_to/_k_sites(78 to 217) are the neighbouring read-scoping concern this recipe's own assigned test file exercises, cited for that reason, not walked as this recipe's Application logic (see When to use, Tests);legacy_router's declaration (263) and its lack of any registered route (see Gotchas).alchemy_labs/backend/tests/test_tracking_scope.py(136 lines, whole file in scope): module docstring (1 to 27, the production incident that motivated the file); every test as listed under Tests.
Supporting (not among the assigned files; read to ground Data model, Deploy & ops and Gotchas, cited explicitly wherever used above rather than folded into the primary claims):
alchemy_labs/backend/models/tracking_model.py29 to 52 (TrackingSitetable definition).alchemy_labs/backend/services/workspace/models.py82 to 96 (WorkspaceSettingstable definition, same column the ad-platform-connections recipe's Data model documents).alchemy_labs/backend/services/workspace/tenant_service.py329 to 355 (update_workspace_settings, the settings-replace behaviour in Gotchas), 405 to 462 (_get_default_settings, the seededtrackingshape in Data model).alchemy_labs/backend/api/routers/workspace_router.py606 to 641 (PATCH /{workspace_id}/settings, the only write path for this recipe's settings keys).alchemy_labs/backend/services/security/oauth_token_crypto.py(decrypt path every read in this recipe goes through; owned and detailed byads/ad-platform-connections).alchemy_labs/backend/migrations/versions/20260504c_tracking_sites_registry.py(the one migration in scope).alchemy_labs/backend/app_fastapi.py956, 957, 960 (the three router mounts groundingmaturity: prodand the surface-exemption claims in Security & tenancy).alchemy_labs/backend/api/dependencies.py187 to 230 (get_current_user), 338 to 349 (get_current_active_user), 680 to 742 (_get_workspace_id).
Deviations of the reference from an ideal implementation, all dated 2026-09-12: POST /api/tracking-sites's
existence check is a cross-tenant oracle, not workspace-scoped; the settings half of this recipe has no dedicated
write endpoint of its own, only a generic whole-object-replace endpoint outside this recipe's scope; the two
conversion-mapping getters each re-fetch and re-decrypt the whole settings dict independently; the legacy_router
mount promises an /api/track alias that does not exist for this endpoint; per-platform "enabled" can read true
off an event-toggle default with zero credentials configured; no test in the repo exercises the registry CRUD's
own status-code behaviour or either mapping getter's own matching logic.
Acceptance checks
Run from alchemy_labs/backend at 62ba4fc with the repo's own .venv. Expected observations are exact, as
measured 2026-09-12.
.venv/bin/python -m pytest tests/test_tracking_scope.py -q --no-cov -o addopts="" -p no:cacheproviderprints14 passed, 0 failed.grep -c "^def test_" tests/test_tracking_scope.pyprints12;grep -c "@pytest.mark.parametrize" tests/test_tracking_scope.pyprints1(test_non_admin_is_never_unrestricted, 3 cases, taking the 12defs to 14 passed).grep -n "site_id = Column(String, primary_key=True)" models/tracking_model.pyshows line39, confirmingsite_id(not a surrogate id) is the table's own primary key, the invariant Purpose and Application logic name.grep -n "workspace_settings.settings = settings" services/workspace/tenant_service.pyshows line350, confirming the one write path for this recipe's settings-dict keys replaces rather than merges (Gotchas).grep -n "legacy_router\." api/routers/tracking_router.pyreturns no match (exit code1), confirming the legacy/api/trackalias carries zero routes.grep -rl "FormConversionMapping\|CalendarConversionMapping" tests/(fromalchemy_labs/backend) returns no match (exit code1), confirming neither dataclass is asserted against anywhere in the test tree.git log -1 --format=%H(run fromalchemy_labs) prints62ba4fc41009b454e1c033e5036c7e8577db2061, andgit branch --show-currentprintsmain, confirmingmeasured_onandmaturity: prod.
Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14. The recipe file is the source; this page is a rendering of it.