Module kit / Analytics and tracking
Conversion Events and Consent Gate
analytics-tracking/conversion-events-and-consent-gate
| Kind | backend |
|---|---|
| Domain | analytics-tracking |
| Maturity | prod |
| Reusable | extract |
| Stack | fastapi-0.118, pydantic-2.12, sqlalchemy-2.0-sync, postgres, httpx-0.28 |
| Depends on | _shared/fastapi-project-layout, _shared/workspace-scoping, _shared/auth-dependency-pattern, _shared/error-envelope, _shared/test-layout |
| Frontends | analytics-tracking/ui/tracking-dashboard |
| Reference implementation |
|
| Measured on | alchemy_labs main 62ba4fc41009b454e1c033e5036c7e8577db2061 2026-09-12 |
| Version | 1 |
Purpose
A first-party conversion (a high-intent click, a lead, a purchase) is written to the audit trail on every call regardless of consent, but is only ever handed to an ad platform when the caller says the visitor granted marketing consent (track_conversion_event, tracking_router.py 1036 to 1438). The invariant that makes it trustworthy is the ordering: the event name is checked against a fixed allowlist first (1091 to 1108), the database write happens next and unconditionally (1113 to 1155), and only after that does the handler look at consent_marketing to decide whether Meta, LinkedIn or GA4 ever see the visitor's data (1157 to 1172) - a caller can never buy its way past the allowlist, and consent can never be inferred from anything except the one boolean field the caller sent.
When to use
Read it before touching track_conversion_event, ALLOWED_EVENT_NAMES, the platform_status dict, _high_intent_sessions, or the workspace-resolution walk (get_workspace_id → TrackingSite → legacy projects join, 1201 to 1237); before adding a new event name to the allowlist or a new platform to platform_status; before changing what consent_marketing gates. Trigger phrases: "why wasn't this conversion forwarded", "add a new conversion event", "the pixel isn't in Meta Events Manager", "no_consent", "no_workspace_resolved", "platform_status", "Unsupported event".
Not this recipe: POST /api/scribe/test-conversion (test_conversion_event, 706 to 1024) is the authenticated debug/verification tool that fires a real or dry-run event per platform and returns the raw payload - covered here only where it shares code with /conversion, not as its own recipe; POST /api/scribe/event (TrackEventRequest, track_event, 538 to 674) is the general page-view/click/form event beacon, a neighbouring recipe, though its background Meta PageView/GA4 forwarding (_forward_ga4_event 505 to 518, _forward_meta_pageview 521 to 535) is the same consent-then-forward shape; the OAuth/token lifecycle behind tracking_config.meta.access_token / tracking_config.linkedin.access_token (tracking_settings_service.py, workspace-settings storage and decrypt_tracking_tokens) belongs to ads/ad-platform-connections, this recipe only reads the resolved config object handed to it; Google Ads forwarding is out of scope because this endpoint never calls it - see API surface.
Stack assumptions
At 62ba4fc: fastapi==0.118.2, synchronous handlers on sqlalchemy.orm.Session (SQLAlchemy==2.0.43), pydantic==2.12.0 for ConversionEventRequest, httpx==0.28.1 inside meta_capi_service.py (async with httpx.AsyncClient(timeout=30.0), line 284) and linkedin_capi_service.py (same pattern, line 284), Postgres via psycopg2-binary==2.9.10. Foundations assumed: S4 (fastapi-project-layout) for mount_router and the app's surface gate - this router is mounted surface_exempt("public", "Scribe beacon - no auth by design") (app_fastapi.py 956); S6 (workspace-scoping) for the concept, but not its usual mechanism - Scribe embeds send only X-Site-ID, never X-Workspace-ID, so WorkspaceMiddleware leaves request.state.workspace_id empty and this handler resolves the tenant itself via a TrackingSite registry lookup with a legacy-join fallback (1201 to 1237, comment at 1201 to 1206 states this explicitly); S5 (auth-dependency-pattern) for get_current_user, used only by /test-conversion (710), never by /conversion itself.
Environment: none dedicated to this endpoint. Per-platform enable/credential state comes from get_tracking_config_service(db, workspace_id) (tracking_settings_service.get_tracking_config, imported line 40), which reads workspace settings first and falls back to env vars (META_PIXEL_ID, META_ACCESS_TOKEN, META_CAPI_ENABLED, LINKEDIN_ACCESS_TOKEN, LINKEDIN_CAPI_ENABLED, LINKEDIN_CONVERSION_ID_HIGH_INTENT, GA4_ENABLED via getattr(settings, 'GA4_ENABLED', True) at 1372) - the credential-resolution mechanics themselves are ads/ad-platform-connections' subject. Fail behaviour: the route itself never raises; every branch (unsupported event name, no consent, dedup, no workspace, a per-platform exception) returns HTTP 200 with a success/platform_status body, and each of the three platform-forward blocks (1274 to 1320, 1334 to 1358, 1376 to 1419) has its own try/except Exception that downgrades a raised error to platform_status[<platform>]['reason'] = f'exception: {e}' rather than letting it propagate - this endpoint does not use the raise-and-let-the-handler-render shape _shared/error-envelope describes for authenticated routes, because a webhook-shaped public beacon must never 500 a visitor's browser.
Data model
No new tables. Reuses tracking_events, tracking_sessions, tracking_sites (models/tracking_model.py):
tracking_sites ( -- 29 to 52
site_id varchar PRIMARY KEY, -- the X-Site-ID header value
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
allowed_origins jsonb DEFAULT '[]',
is_active boolean NOT NULL DEFAULT true
)
tracking_sessions ( -- 55+
id uuid PRIMARY KEY,
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
anonymous_id varchar UNIQUE NOT NULL,
contact_id uuid -- set once identified
)
tracking_events ( -- 214 to 260
id uuid PRIMARY KEY,
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
session_id uuid NOT NULL REFERENCES tracking_sessions(id),
contact_id uuid REFERENCES tracking_contacts(id),
event_type varchar NOT NULL, -- 'conversion_highintent', 'conversion_purchase', ...
event_data jsonb, -- see below
page_url varchar, page_path varchar, page_title varchar,
site_id varchar,
created_at timestamptz DEFAULT now()
)
-- Index('ix_tracking_events_contact_created', contact_id, created_at)
-- Index('ix_tracking_events_session_type', session_id, event_type)
event_type is written as f"conversion_{event_name.lower()}" (1136). event_data is built once at persist time ({**event_data, 'event_id', 'consent_marketing', 'has_fbclid', 'has_gclid', 'has_li_fat_id'}, 1137 to 1144) and then mutated in place after forwarding completes to add forwarded_to and the full platform_status dict (1421 to 1427) - one row, two writes, both inside the same request.
The persistence is conditional, not unconditional, despite the section-header comment at 1113 to 1114 ("PERSIST EVENT TO DATABASE (always, regardless of consent)"): _persist_event (1119 to 1153) only inserts a TrackingEvent row when a TrackingSession already exists for data.anonymous_id and that session already has a resolved workspace_id (1130); otherwise it logs a warning and returns None (1151 to 1152), and the response's db_event_id is null. "Always" means "always regardless of consent", not "always regardless of whether a session is known" - worth stating precisely because the docstring's own wording overclaims.
What is NOT stored: no dedicated consent table or column - consent_marketing lives only inside event_data, on the row itself, not as a first-class field; no PSP/ad-platform response body - platform_status['*']['reason'] stores a short code or f'api_error: {result.error}'/f'exception: {e}', never a raw HTTP body.
API surface
Both routes live on router = APIRouter(prefix="/api/scribe", ...) (260); neither is duplicated on legacy_router (/api/track, 263).
| Route | Auth | Tenancy | Body | Codes | Notes |
|---|---|---|---|---|---|
POST /api/scribe/conversion (track_conversion_event, decorator 1035, def 1036) | none - surface_exempt("public", "Scribe beacon - no auth by design"), app_fastapi.py 956 | workspace_id resolved server-side from X-Site-ID via the TrackingSite registry, with a legacy projects join fallback (1215 to 1235); never trusted from a client-sent workspace id | ConversionEventRequest (366 to 398): event_name: str, anonymous_id/fingerprint/email/phone optional, event_id: Optional[str] (client-supplied for dedup, else a fresh UUID, 1064), event_data: Optional[Dict], page_url, fbclid/fbc/fbp/gclid/li_fat_id, ga_client_id, consent_marketing: bool = False, site_id | always HTTP 200; body carries success: bool and, on the happy path, platform_status: Dict[str, Dict] - the endpoint never raises an HTTPException | An unrecognized event_name returns {"success": false, "message": "Unsupported event: ... Allowed: [...]"}" at 200, not 422/400 (1103 to 1108) |
POST /api/scribe/test-conversion (test_conversion_event, decorator 706, def 707) | get_current_user (710) - Bearer JWT, httpOnly cookie, or X-API-Key, in that order (dependencies.py 187 to 199) | none of its own - reads get_workspace_id(request) (734), the session-resolved workspace of the authenticated caller | TestConversionRequest (681 to 703): test user fields, event_name: str = "Lead", click ids, platforms: List[str] = ["meta","linkedin","google_ads","ga4"], dry_run: bool = False | 200 with a results dict per platform plus a summary; 401 via get_current_user's HTTPException(status_code=401, detail="Could not validate credentials") (dependencies.py 195 to 199) on missing/invalid credentials | Unlike /conversion, this route DOES call Google Ads (google_ads.upload_lead_conversion, 927 to 932) - /conversion never does (see below) |
Debug/read surface for what /conversion wrote: GET /api/scribe/sessions/{session_id}/events (2594 to 2637) returns every TrackingEvent.event_data for a session (so a forwarded conversion's platform_status is visible there), gated by get_current_user plus a site-ownership check for non-admins (2614 to 2620); 404 if the session doesn't exist or is internal, 403 if a non-admin's allowed site ids don't include the session's site_id.
Application logic
track_conversion_event, in the order the source runs it:
- Allowlist gate (1091 to 1108):
ALLOWED_EVENT_NAMES = {'HighIntent', 'ViewContent', 'PageExit', 'ArticleRead60', 'ArticleRead120', 'InitiateCheckout', 'Purchase', 'Lead', 'Contact', 'CompleteRegistration', 'Schedule'}(11 names). Anything else returns{"success": false, "message": "Unsupported event: ..."}"immediately - no DB write, no forwarding attempt. - Persist (1113 to 1155, run in a threadpool so the event loop isn't blocked): find the
TrackingSessionbyanonymous_id; only if found and it already carries aworkspace_iddoes aTrackingEventget inserted and flushed (not committed yet). - Consent gate (1157 to 1172):
if not data.consent_marketing, everyplatform_status[*]['reason']is set to'no_consent', the persisted event is committed as-is, and the response returns withforwarded_to: []. Nothing past this line runs withoutconsent_marketing is True. - HighIntent dedup (1174 to 1199): for
event_name == 'HighIntent'only, a keyf"{anonymous_id or fingerprint or 'unknown'}:{event_data.get('trigger','unknown')}"is checked against the in-process dict_high_intent_sessions(module-level, line 1032); a hit inside a one-hour window (3600seconds) short-circuits withreason: 'deduplicated'for meta and linkedin and commits without forwarding. - Workspace resolution (1201 to 1258):
get_workspace_id(request)first (empty for Scribe embeds); else, ifsite_idis present and not"unknown", a threadpooled lookup tries theTrackingSiteregistry first, then a legacyprojects JOIN workspaces ON workspaces.owner_id = projects.user_id WHERE projects.tracking_site_id = :sidas a fallback (1224 to 1231). If neither resolves, every platform is marked'no_workspace_resolved'and nothing is forwarded - the code's own comment (1239 to 1242) names this a deliberate anti-cross-tenant-leak control: it refuses to fall back to a global env-var pixel/token, because that would forward one client's conversion into another client's ad account. - Per-platform forward, each independently gated and independently try/excepted:
- Meta (1263 to 1320): skipped with
'platform_disabled'iftracking_config.meta.enabledis false, or'no_pixel_id_configured'if pixel_id/access_token are missing; else callsmeta_capi.send_event(...), settingcontent_category/content_idsforViewContentandcontent_category: 'engaged_visit'forPageExit(1281 to 1288). - LinkedIn (1322 to 1358): only
event_name == 'HighIntent'is forwarded - every other event name is markedf'event_type_not_supported ({event_name})'(1327 to 1328); else'platform_disabled'or'no_conversion_rule_configured'; elselinkedin_capi.send_high_intent_event(...). - Google Ads: never called from this endpoint.
platform_status['google']is initialised to{'sent': False, 'reason': 'not_supported_for_event_type'}(1070) and is only ever overwritten with the same shape of message (1365); the "Google Ads Enhanced Conversions only support Lead-like events" comment (1361 to 1364) documents the decision, but no code path in this handler sends a Lead-typed event to Google at all -/test-conversionis the only route in this file that callsgoogle_ads.upload_lead_conversion. - GA4 (1368 to 1419): skipped with
'platform_disabled'ifGA4_ENABLEDis false, or'not_configured (missing GA4_MEASUREMENT_ID or GA4_API_SECRET)'; elsega4_service.send_event(...)withclient_idpreferringdata.ga_client_id, falling back tofingerprint,anonymous_id, then a synthesizedanon_<event_id[:8]>.
- Meta (1263 to 1320): skipped with
- Record and return (1421 to 1438): the persisted
tracking_event.event_datais updated withforwarded_toand the fullplatform_status, committed, and returned alongsidesuccess: true.
Invariants
- An event name outside
ALLOWED_EVENT_NAMESnever reaches persistence or forwarding (1103 to 1108). consent_marketingis the only signal that gates forwarding; it is read once (1158) and every platform'splatform_status['reason']is set to'no_consent'before any outbound call is attempted (1160 to 1163).- A conversion is never forwarded to a workspace's ad accounts using another workspace's or the environment's default pixel/token - an unresolved workspace stops all forwarding rather than falling back to a global credential (1239 to 1249, the code's own stated reason: "a GDPR/nDSG controller-leak bug").
- A failure inside one platform's forward call never prevents the other platforms from being attempted or the response from being returned - each of the three forward blocks has its own
try/except Exception(1318 to 1320, 1356 to 1358, 1417 to 1419). - The route itself never raises: every exit is a
return {...}with HTTP 200, including the unsupported-event-name case.
Frontend
analytics-tracking/ui/tracking-dashboard the surface that would render forwarded_to /
platform_status for a given session or contact;
at 62ba4fc the only backend read this recipe's
output has is GET /api/scribe/sessions/{id}/events
(2594 to 2637), which returns raw event_data
(platform_status included) with no dedicated
per-platform summary view - a rebuild of the
dashboard is the first thing that would need to
aggregate platform_status across events itself
No dedicated admin/debug route exists for "conversions forwarded in the last N minutes" or similar; the nearest thing is reading tracking_events.event_data->>'platform_status' directly, or the per-session /sessions/{id}/events route above.
Security & tenancy
Auth dependency: none on /conversion (surface_exempt("public", ...), app_fastapi.py 956) - by design, since this is the endpoint scribe.js calls from an anonymous visitor's browser; get_current_user on /test-conversion (710) and on the debug read route /sessions/{id}/events (2596 to 2597), both requiring a session Bearer/cookie or API key.
Tenancy: /conversion derives workspace_id from X-Site-ID through the TrackingSite registry (server-side, trusted table), never from a client-supplied workspace id - there is no X-Workspace-ID header on this path at all (comment, 1201 to 1206). Fail direction is closed: no resolvable workspace means zero forwarding to any platform (1243 to 1258), not a fallback to a default/shared credential.
Secrets at rest: tracking_config.meta.access_token / tracking_config.linkedin.access_token come from WorkspaceSettings.settings['tracking'], read through decrypt_tracking_tokens (tracking_settings_service.py 83 to 85, "Dual-read decrypt: ciphertext decrypted, legacy plaintext untouched" - the source's own comment, meaning some rows are still stored as legacy plaintext at this commit); this recipe only consumes the already-decrypted config object and never logs it - platform_status['reason'] values are short codes or str(exception), never a token or a PSP response body. PII in the outbound calls is SHA-256 hashed before leaving the process (meta_capi_service._build_user_data, 102 to 162: em/ph/fn/ln/country/external_id all hashed, fbp/fbc/IP/UA passed through unhashed per Meta's spec).
Rate limits: none found dedicated to /api/scribe/conversion (app_fastapi.py's RateLimitExceededError handler at 645 to 646 is global; nothing in tracking_router.py or its mount applies a limiter to this route specifically) - the endpoint's only throttle is the in-process HighIntent dedup window, which is not a rate limit and does not apply to any other event name.
Attacker-controlled inputs, all unauthenticated on /conversion: event_name (checked against the allowlist, everything else refused); consent_marketing (a caller can send true with no real consent - this endpoint trusts the caller's claim, it does not itself verify consent was captured, which is analytics-tracking's consent-capture surface's job, not this one's); site_id (only resolves to a workspace if it matches an active TrackingSite.site_id, an unregistered value simply resolves to no workspace, per Application logic step 5); every click id and event_data value (forwarded largely as-is to the ad platforms after PII hashing, not otherwise validated for shape beyond Pydantic's Optional[str]/Optional[Dict[str, Any]] typing).
Tests
The test file this recipe was scoped against, alchemy_labs/backend/tests/unit/test_checkout_tracking_allowlist.py, does not test ALLOWED_EVENT_NAMES or the consent gate in tracking_router.py - it tests a different, adjacent allowlist: _TRACKING_KEYS in schemas/commerce_schemas.py (363 to 371), which restricts which keys survive in CheckoutRequest.tracking, the free-form attribution dict POST /api/commerce/checkout accepts from an unauthenticated caller. The module docstring (1 to 8) states its own scope plainly: guarding against orders.extra_metadata becoming "an unauthenticated arbitrary-write primitive" and "a way for us to become a PAN store by accident" - a payload-key allowlist on a different router, not the event-name allowlist this recipe documents. This is a genuine gap: no test file in the repo directly exercises ALLOWED_EVENT_NAMES, the consent_marketing gate, or the platform_status envelope shape on /api/scribe/conversion (confirmed by grep -rln "consent_marketing\|Unsupported event\|ALLOWED_EVENT_NAMES" backend/tests/, which returns only a test that mocks track_conversion_event outright rather than exercising its allowlist logic).
Run 2026-09-12 with alchemy_labs/backend/.venv, no database required (pure Pydantic validation):
python -m pytest tests/unit/test_checkout_tracking_allowlist.py -q --no-cov -o addopts="" -p no:cacheprovider
prints 14 passed (7 def test_ functions, one parametrized over 8 field names, 32 Pydantic-deprecation warnings unrelated to this recipe). Hermetic: no network, no database, no fixtures beyond _req() building a CheckoutRequest directly.
Known-answer vectors from that file (all against _TRACKING_KEYS, not ALLOWED_EVENT_NAMES): card_number/pan/cvc/cvv/exp_month/exp_year/cardholder/iban are all silently dropped, never rejected (test_payment_fields_are_dropped); an unknown key is dropped, not a 422 (test_unknown_keys_dropped_not_rejected); a nested dict/list value is dropped (test_nested_structures_rejected); a string value is clamped to 512 characters (test_values_are_length_clamped); _TRACKING_KEYS itself contains no substring matching card/pan/cvc/cvv/iban/cardholder/exp/credential/password (test_allowlist_contains_no_payment_field).
Deploy & ops
Config keys: none new. Per-platform tokens (META_ACCESS_TOKEN, LINKEDIN_ACCESS_TOKEN, etc.) are read but not owned by this recipe - see When to use. Migrations: none - reuses tracking_events/tracking_sessions/tracking_sites, which predate this endpoint. Scheduler: none. The HighIntent dedup (_high_intent_sessions, line 1032) is a plain module-level Python dict, not Redis- or DB-backed, and the production Dockerfile runs uvicorn ... --workers ${WEB_CONCURRENCY:-2} (line 117) - each worker process holds its own copy, so a HighIntent event routed to a different worker within the one-hour window is not deduplicated against the first (see Gotchas). Argus-style checks: none found dedicated to this endpoint (searched tracking_router.py and the router mount for a limiter or health probe specific to /conversion; found none). Rollback: stateless - no migration to unwind; a rolled-back deploy loses nothing already committed to tracking_events.
Gotchas
- 2026-09-12 (measured at 62ba4fc,
tracking_router.py1032, Dockerfile 117): the HighIntent dedup dict is per-process, and prod runs 2 to 4 uvicorn workers (--workers ${WEB_CONCURRENCY:-2}inDockerfile,--workers 4inDockerfile.filemanager). A rebuild that assumes "at most one HighIntent forward per session per hour" is only true per worker, not per deployment, unless this moves to a shared store. - 2026-09-12 (measured at 62ba4fc, 1113 to 1152): the "PERSIST EVENT TO DATABASE (always, regardless of consent)" comment overstates what the code does - persistence additionally requires a pre-existing
TrackingSessionwith a resolvedworkspace_id; a conversion for an anonymous_id with no session yet, or a session whose workspace never resolved, is silently not persisted (only a warning log), withdb_event_id: nullin the response. - 2026-09-12 (measured at 62ba4fc, 1360 to 1366):
platform_status['google']exists in the envelope's shape on every response but is never set to anything other than "not supported for this event type" from/conversion- a caller reading the envelope and seeing agooglekey present could reasonably assume Google Ads forwarding is attempted and just not configured; it is never attempted from this route at all. - 2026-09-12 (measured at 62ba4fc, tests directory grep): no test in the repository exercises
ALLOWED_EVENT_NAMES, the consent gate, or theplatform_statusshape directly; the nearest coverage (test_booking_g5_calendly_router_...py) mockstrack_conversion_eventoutright rather than calling into it.
Reference implementation
Paths relative to the repo root; all at main 62ba4fc41009b454e1c033e5036c7e8577db2061 unless noted.
alchemy_labs/backend/api/routers/tracking_router.py(3576 lines): router declarations (260, 263);ConversionEventRequest(366 to 398);TestConversionRequest(681 to 703);test_conversion_event(706 to 1024);_high_intent_sessions(1032);track_conversion_event(1035 to 1438);get_session_events(2594 to 2637).alchemy_labs/backend/services/meta_capi_service.py(414 lines, whole file in scope):MetaCAPIService(39 to 414);_build_user_data(102 to 162, PII hashing);_sanitize_custom_data(164 to 183);send_event(189 to 342).alchemy_labs/backend/services/linkedin_capi_service.py(418 lines, whole file in scope):LinkedInCAPIService(31 to 418);send_event(157 to 334);send_lead_event(336 to 376);send_high_intent_event(378 to 414).alchemy_labs/backend/models/tracking_model.py:TrackingSite(29 to 52);TrackingSession(55+);TrackingEvent(214 to 260).alchemy_labs/backend/services/tracking_settings_service.py:MetaTrackingConfig/LinkedInTrackingConfig/TrackingConfigdataclasses (21 to 58);get_workspace_tracking_settings(67 to 90, the dual-read decrypt at 83 to 85);get_tracking_config(93+).alchemy_labs/backend/api/dependencies.py:get_current_user(187 to 199+).alchemy_labs/backend/app_fastapi.py: the mount and its public surface exemption (956 to 957).alchemy_labs/backend/schemas/commerce_schemas.py:_TRACKING_KEYS(365 to 371), the allowlist the given test file actually covers, cited here only to show what it is NOT this recipe's subject.- Test:
alchemy_labs/backend/tests/unit/test_checkout_tracking_allowlist.py(62 lines, whole file in scope).
Deviations of the reference from an ideal implementation, all dated 2026-09-12: no direct test coverage of ALLOWED_EVENT_NAMES/the consent gate exists; the HighIntent dedup is in-process and not safe across the prod worker count; platform_status['google'] is a permanent placeholder, never a real attempt; the "always" in the persist step's own comment is inaccurate for sessionless calls; some access_token rows are still legacy plaintext per the dual-read comment in tracking_settings_service.py.
Acceptance checks
Run from alchemy_labs/backend at 62ba4fc (or an app built to this recipe). Expected observations are exact, as measured 2026-09-12.
grep -n "ALLOWED_EVENT_NAMES = {" -A 12 api/routers/tracking_router.pyshows the set opening at line 1091 and closing (}) at line 1102, eleven names total.python3 -c "print(sorted({'HighIntent','ViewContent','PageExit','ArticleRead60','ArticleRead120','InitiateCheckout','Purchase','Lead','Contact','CompleteRegistration','Schedule'}))"prints['ArticleRead120', 'ArticleRead60', 'CompleteRegistration', 'Contact', 'HighIntent', 'InitiateCheckout', 'Lead', 'PageExit', 'Purchase', 'Schedule', 'ViewContent'], matching the allowlist's own sorted order used in the 200 error message at line 1106.grep -n "APIRouter(prefix=\"/api/scribe\"" api/routers/tracking_router.pyprints line260;grep -n "surface_exempt(\"public\", \"Scribe beacon - no auth by design\")" app_fastapi.pyprints line956- confirms/conversionis public by design, not an oversight..venv/bin/python -m pytest tests/unit/test_checkout_tracking_allowlist.py -q --no-cov -o addopts="" -p no:cacheproviderprints14 passed(0 failed).grep -rln "consent_marketing\|Unsupported event\|ALLOWED_EVENT_NAMES" tests/prints exactly one file,tests/v2/coverage/test_booking_g5_calendly_router_webhooks_signature_verification_scribe_bridge_site_routing.py, and that file's own matches are all against apatch(...)-mockedtrack_conversion_event, not the real allowlist - confirms the Tests-section gap.grep -n "\-\-workers" Dockerfileprints--workers \${WEB_CONCURRENCY:-2}on line117, confirms the multi-worker Gotcha about_high_intent_sessions.
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.