feat(analytics): Plausible funnel goals and storage-free attribution #673
No reviewers
Labels
No labels
API
Billing
UI/UX
dependencies
bug
change
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
servala/servala-portal!673
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/plausible-attribution"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
servala.com and the VSHN landing pages send visitors to the portal with
refandutm_*parameters. The portal lost them at the first redirect: an anonymous deep link such as/provision/postgresql/?ref=codey_chwent to/accounts/login/?next=..., whererefsits insidenext. Plausible reads the source from the first pageview's URL only, so these visits counted as Direct.This PR makes the funnel from landing page to servala.com to portal order traceable, without storing anything on the visitor's device.
Attribution survives redirects
AttributionRedirectMiddlewarecopiesref,utm_source,utm_medium,utm_campaign,utm_term,utm_contentandgclidonto every same-host redirect of a GET request. It never overwrites a value the target already has, and it leaves external redirects (Keycloak) alone./accounts/login/?next=...&ref=codey_ch. Deep link parameters (provider,plan,instances,sla,storage,zone) pass through unchanged.Plausible goals on confirmed outcomes
UserSignedUpuser_signed_up(first Keycloak login creates the account)UserLoggedInuser_logged_inOrganizationCreatedapproval:automaticorpendingServiceInstanceCreatedservice,planServiceInstancePlanChangedservice,planCTA Clicka.btn(browser)label,sectionForm Startedform[data-analytics-form](browser)source:Service Order,Organization Createjson_script, on the next full page. A reload or a failed submission never fires them.UserLoggedIn.static/js/analytics.js, so buttons and forms rendered later by htmx are covered.Form Startedfires once per form name per page load, because htmx replaces the order form on every plan or zone change.Attribution into Odoo
ref,utm_source,utm_medium,utm_campaignand the external HTTP referrer as hidden fields.source_id,medium_idandcampaign_idlooked up by name, created when missing, and otherwise the optional defaultsSERVALA_ODOO_UTM_SOURCE_ID,SERVALA_ODOO_UTM_MEDIUM_ID,SERVALA_ODOO_UTM_CAMPAIGN_ID.Referrer: <url>andReferred from: <ref>are appended tointernal_note, HTML-escaped and capped at 200 characters.pending_billing_data["attribution"]and replay them when staff approve.Deployment
sync_plausible(companion change in vshn/landingpager).SERVALA_ODOO_UTM_*_IDvariables. Without them, sale orders without campaign parameters leave the UTM fields empty, as today.Testing
src/tests/test_attribution.py: redirect chain for anonymous, single-org and no-org visitors, the middleware's edge cases, referrer filtering (own host, SSO host), hidden fields, Odoo payload with lookup, create, defaults, a failed UTM lookup, and replay on approval.src/tests/test_plausible.py: queueing, one-time rendering, escaping, a real password login renderingUserLoggedInon the landing page, organization creation with both approval modes, and no goal for an invalid form.static/js/analytics.jswas checked in a browser harness that loads the real file with a recordingplausiblestub and asserts the exact event list (nav, section and footer buttons, aria-label fallback, 60 character cap, non-button links ignored, late-added button, htmx-style form swap).Design Decisions
main. "rebase 673 on 674".utm_term,utm_contentandgclidon redirects, but only three UTM values into Odoo: Plausible reads all of them as sources; Odoo sale orders only have source, medium and campaign.pending_billing_data: the sale order only exists after approval, so the values must wait somewhere server-side. This is database storage of attribution tied to a customer record, flagged for the same legal check.django.contrib.auth: hijack logs in throughdjango.contrib.auth.login, and staff impersonation must not count as a login.UserSignedUpdoubles as first login: the portal account is created on the first Keycloak login, so a separate first-login goal would always equal it.Spec Diff
Compared with the handoff:
json_scriptfor props, delegatedCTA ClickandForm Started, hidden form fields for attribution, UTM lookup by name with create and default fallback, escapedReferrer:andReferred from:capped at 200 characters, tests including one-time firing, reload and failed submission.<head>already existed (withplausible.init), so it was kept rather than replaced. Goals render from a session queue instead of a per-view flag, because several can be pending at once (for exampleUserSignedUpandUserLoggedIn).internal_noterather than a lead description.OrganizationCreatedwith anapprovalprop; replay on approval.Servala Portal Clickdoes not apply inside the portal. An order-submitted goal separate fromServiceInstanceCreatedwas not added, since instance creation is the confirmed outcome of the order form.🤖 Generated with Claude Code
https://claude.ai/code/session_014S5eLBkR9DQyhYAG11hBZw
49e8dc5305a00f26c726Before we can continue with that:
src/servala/core/odoo.py-> we need to make sure the Odoo user has access to the referenced modelsSERVALA_ODOO_UTM_*variable values indeployment/kustomize/*a00f26c726bd3ce72b86