feat: add premium paywall conversion telemetry - #28425
Merged
Merged
Conversation
Contributor
Docs previewCheck off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here. |
david-fraley
force-pushed
the
dfraley/premium-paywall-telemetry
branch
2 times, most recently
from
August 20, 2026 23:46
d52d44e to
3cdb6d1
Compare
david-fraley
force-pushed
the
dfraley/premium-paywall-telemetry
branch
from
August 21, 2026 00:07
3cdb6d1 to
3f37813
Compare
david-fraley
force-pushed
the
dfraley/premium-paywall-telemetry
branch
from
August 21, 2026 00:27
3f37813 to
c4c2042
Compare
jeremyruppel
approved these changes
Aug 21, 2026
david-fraley
force-pushed
the
dfraley/premium-paywall-telemetry
branch
from
August 21, 2026 00:50
c4c2042 to
c378378
Compare
david-fraley
marked this pull request as ready for review
August 21, 2026 13:39
stack merge was automatically disabled
August 21, 2026 13:52
Pull Request is not mergeable
stack merge was automatically disabled
August 21, 2026 13:52
Pull Request is not mergeable
stack merge was automatically disabled
August 21, 2026 13:54
Pull Request is not mergeable
Report which premium paywall a user clicked through, and report the trial signups that follow, so the funnel from paywall to trial can be measured. Clicks are reported by the browser and carry a UUID that is stored in sessionStorage. The premium page sends that UUID with the trial request, and coderd emits the trial_signup event itself once a license is actually issued, so a conversion cannot be forged by a client. Joining signups back to clicks on the attribution ID separates completed trials from abandoned ones, and trials started without a paywall report the "direct" source. Sources are a fixed enum rather than page URLs because paywall routes embed organization and template names.
david-fraley
force-pushed
the
dfraley/premium-paywall-telemetry
branch
from
August 21, 2026 14:13
1a7fc64 to
cad94f1
Compare
jeremyruppel
approved these changes
Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR: When someone using free Coder taps a "Start trial for free" button, we now write down which page they tapped it on, and whether they went on to actually start the trial. That tells us which locked features make people want Premium.
Brief summary
appearanceoraudit_log), never the web address, so no customer names are collected.#28226 added the trial form and has since merged, so this now targets
main.Description
Today the "Start trial for free" button just moves you to the Premium page and nothing is recorded, so there is no way to know which feature convinced someone.
This adds two events:
cta_clicktrial_signupThe click generates a random ID. It rides along to the trial form and gets saved on the signup, so a click and a signup can be matched up later. Clicks with no matching signup are the people who gave up. Trials started without a paywall (for example straight from the sidebar) are labelled
direct, so "no label" never gets confused with "broken".A few small choices worth knowing:
There is deliberately no "paywall was shown" event. Every admin page visit would report one, which is a lot of noise for little insight.
Every paywall in the Premium Copy Updates table has a label. The rows that do not are badges and alerts with no button to click.
Proof
Ran a local Coder with no licence and clicked the paywall button on the Appearance page.
What the browser sent, and the reply:
What the browser saved for the trial form, same ID:
{"id":"92d96afa-f975-4789-b8a7-b1fd19d4df85","source":"appearance","createdAt":1787268520857}The click then landed on the Premium page with the trial form. Screenshots of the network panel, the saved value, and the Premium page are attached below. The form itself was not submitted, since that would contact the real licence server.
Testing
directfallback.make genis clean, and lint, types, and formatting pass.Two stories fail on
maintoday and fail the same way here:ExternalAuthSettingsPageView > PageandPremiumPageView > No License. Neither file is touched by this branch; both were verified failing onmainwith this branch checked out elsewhere.Follow-up
coder/coder-telemetry-server#45 saves these events for reporting. It merges after this one.
Plan and decisions
Goal: find out which Premium paywall drives trial signups, and tell abandons apart from completions.
Options considered and dropped:
?from=appearancein the URL. Rejected: messy for users.Event shape:
Where the code lives: nothing under
components/imports frommodules/, so the paywall components stay presentational and only gain anonCTAClickprop. New wrappers inmodules/paywall/own the reporting, one per paywall style, each taking a fixedsource.Self-review: the
frontend-reviewskill caught that reading permissions inside the wrapper forced an auth context into presentational page views, which broke their stories. Every call site already passescanViewPremium={permissions.viewAllLicenses}and the button only renders when that is true, so the wrapper no longer reads permissions and the server stays the gate. It also flagged one unattributed paywall on the Security page, now wired up asbrowser_only.