feat(careers): job board + application widget as an opt-in stats.js module - #183
Conversation
…odule Customers already paste the stats snippet. Flipping "Load careers widget" in the dashboard makes that same snippet lazy-load /careers.js, which paints a job board and an inline application form on their /careers page. No second script tag. The cost discipline that makes this acceptable on every page of every customer site: stats.js only fetches the module when the page actually looks like a careers page — a [data-cp-careers] container, or a pathname match. Everything else pays one querySelector. data-careers="off" and data-careers-path="/jobs" are the escape hatches. A client-rendered job board is invisible to crawlers, which is the exact failure CrawlProof audits for. So the widget also writes JobPosting JSON-LD into the host page, and every open role gets a server-rendered canonical page at /c/<project_id>/<slug>. Google for Jobs and answer engines get real HTML either way. Applications are three fields and a link — no resume upload, so we never take custody of a file. They land in an applicant inbox where a reviewer can shortlist, accept, or reject, filtered by status and role. Workplace is remote/hybrid/onsite rather than a remote boolean: hybrid is the case a boolean forces you to misreport, and each value maps to different schema.org output. Tests cover the pure helpers plus the two hand-written browser scripts, which the compiler never parses — a typo there would ship straight to every customer's site.
serveAd diverts HOUSE_AD_ROTATION_RATE (10%) of otherwise-fillable requests to the house ad, which is unmetered and carries no clickUrl. Four of the tests in this file assume a paid fill, so each failed ~10% of the time — about a 1-in-4 chance of a red run on a branch that changed nothing near ads. Measured on an untouched tree: 4 failures in 15 runs; 0 in 15 with the rotation pinned off. The failure surfaced as ".toMatch() expects to receive a string, but got undefined" on whichever of the four lost the coin flip, which is why it moved around between runs. Rotation itself is covered by tests/contract/ads-house-rotation.test.ts, so nothing is lost by making this file deterministic.
ThreatCrush Security Scan52 finding(s) HIGH/CRITICAL: 10 | MEDIUM: 42
…and 2 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
Why there's an unrelated ads commit hereCI went red on
Measured on an untouched tree:
Fix pins rotation off for that file only ( Happy to split that commit into its own PR if you'd rather keep this one pure — it just had to be fixed for this branch to go green. Also merged latest |
serveAd diverts HOUSE_AD_ROTATION_RATE (~10%) of otherwise-fillable requests to the house ad, which is unmetered: no impression row, so no short code and no /a/ click URL. Four of the five tests in this file read fields off whatever came back, so each failed on ~10% of runs — about one CI run in three went red, on whichever test happened to draw the house ad. Measured on master before this change: 6 of 15 runs failed, across four different tests. That is what took down the builds on #179 and #183, neither of which touches ads. These tests are about the click URL of a *paid* fill, so the draw is pinned above the threshold. Fixing Math.random is safe precisely here: the auction has a single candidate either way, and short codes come from crypto.randomBytes, so "issues a distinct code per paid fill" still exercises real entropy — and now checks all 60 fills instead of a subset that silently shrank whenever the house ad turned up. Pinning the draw would have deleted the only coverage of the rotation, so it is asserted directly rather than left to chance: one test for a draw under the rate, one for a draw exactly on it. The boundary is the interesting part, and sampling the rate would just be a slower way to reintroduce a flaky test. 40 consecutive runs of the file now pass, against 9 of 15 before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#183 de-flaked tests/contract/ads-short-code-serving.test.ts by mocking HOUSE_AD_ROTATION_RATE to 0 for that file, which was the right call — the tests there are about the click URL of a paid fill, and the coin flip made roughly one CI run in three go red on whichever test lost it. That commit notes rotation is still covered by ads-house-rotation.test.ts. It isn't, quite: that file calls houseFill() directly and never goes through serveAd, so it pins what a house ad looks like, not when one is served. With the rate pinned to 0 in the only file that reached it, the branch at lib/ads/serve.ts:213 has no test left. That branch is what keeps the network advertising itself on slots that are already selling, and it is one comparison — the kind of thing that can be deleted or inverted in a refactor and show up as a revenue question months later, not a red build. So it is pinned here against serveAd, by driving the draw to each side of the threshold rather than sampling a ~10% frequency. Sampling would mean reintroducing exactly the coin flip that made the other file flaky; a controlled draw tests the same behaviour and cannot fail intermittently. Verified by mutation: '<' to '<=' fails 1 test, inverting the comparison fails 3, deleting the branch fails 2. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a careers module to the drop-in tracker, modelled on the pattern at digipowerx.com/careers — click a role, it expands inline, three fields and a link to apply.
How it ships
Customers already paste the stats snippet. Flipping Load careers widget (Stats → Careers, or the button on the Stats overview) makes that same snippet lazy-load
/careers.js. No second script tag.The cost discipline that makes this acceptable on every page of every customer site:
stats.jsonly fetches the module when the page actually looks like a careers page — a[data-cp-careers]container, or a pathname match on/careers. Every other pageview pays onequerySelectorand a string compare.data-careers="off"anddata-careers-path="/jobs"are the escape hatches.Mount order:
[data-cp-careers]→#careers→<main>→<body>, so "drop in one script and my /careers page auto-vivifies" works with no markup changes.Crawlability is the point, not a bonus
A client-rendered job board is invisible to crawlers — the exact failure CrawlProof audits for. Shipping a naive embed would mean selling against our own product. So:
JobPostingJSON-LD graph into the host page/c/<project_id>/<slug>, with a board index at/c/<project_id>Google for Jobs and answer engines get real HTML either way. This is the thing Greenhouse/Lever embeds structurally can't do.
Applicants
Three fields and a link — name, email, portfolio/LinkedIn/GitHub. No resume upload, so we never take custody of a file. Applications land in an inbox where a reviewer can shortlist / accept / reject (with undo), filtered by status and by role. Re-submitting the same email for the same role upserts instead of duplicating.
workplaceisremote | hybrid | onsiterather than a boolean — hybrid is the case a boolean forces you to misreport, and each value maps to different schema.org output (remote →TELECOMMUTEwith nojobLocation; hybrid → both; onsite →jobLocationonly).Surface
/careers.js/api/careers/jobs/api/careers/apply/c/[project],/c/[project]/[slug]/projects/[id]/stats/careersNothing is served unless the project has both
tracker_enabledandcareers_enabled— enforced in thepublic_job_postingsSECURITY DEFINER function, not just in the route.Checks
tsc --noEmitcleanvitest run— 1206 passed, 1 file skipped (38 new tests; suite was 1195 before)next buildsucceeds; all six new routes registered/careers.jsand/stats.jsare hand-written JS in template literals that the compiler never parses, sotests/careers-widget-script.test.tsparses the served body withnew Functionand asserts the gating/escaping/JSON-LD markers. A syntax error there would otherwise ship straight to every customer's site.Not covered
No DOM test environment is installed (
environment: "node", no jsdom/happy-dom), so the widget's rendering and submit flow are not exercised — only parsed and asserted on content. Worth a follow-up if you want that, but it means adding a dev dependency.The migration has not been applied to any environment.