Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ jobs:
uses: ./.github/actions/setup-node-pnpm

- name: Install Chromium
run: pnpm --filter @lvbt/analytics exec playwright install --with-deps chromium
run:
pnpm --filter @lasvegasfortransit/analytics exec playwright install --with-deps chromium

- name: Check
run: pnpm check
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/deploy-collector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ jobs:
uses: ./.github/actions/setup-node-pnpm

- name: Install Chromium
run: pnpm --filter @lvbt/analytics exec playwright install --with-deps chromium
run:
pnpm --filter @lasvegasfortransit/analytics exec playwright install --with-deps chromium

- name: Validate
run: pnpm check

- name: Deploy
run: pnpm --filter @lvbt/analytics-collector exec wrangler deploy
run: pnpm --filter @lasvegasfortransit/analytics-collector exec wrangler deploy
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Expand Down
15 changes: 9 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ name: Publish package
on:
release:
types: [published]
workflow_dispatch:

permissions:
contents: read
id-token: write
packages: write

jobs:
publish:
name: Publish package
runs-on: ubuntu-latest
timeout-minutes: 15
environment: npm
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -24,16 +24,17 @@ jobs:
uses: ./.github/actions/setup-node-pnpm

- name: Install Chromium
run: pnpm --filter @lvbt/analytics exec playwright install --with-deps chromium
run:
pnpm --filter @lasvegasfortransit/analytics exec playwright install --with-deps chromium

- name: Validate
run: pnpm check

- name: Configure npm registry
- name: Configure GitHub Packages
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: package.json
registry-url: https://registry.npmjs.org
registry-url: https://npm.pkg.github.com

- name: Confirm release version
run: |
Expand All @@ -42,4 +43,6 @@ jobs:

- name: Publish with provenance
working-directory: packages/analytics
run: pnpm publish --access public --no-git-checks
run: pnpm publish --access restricted --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3 changes: 2 additions & 1 deletion .github/workflows/weekly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ jobs:
run: pnpm build

- name: Install Chromium
run: pnpm --filter @lvbt/analytics exec playwright install --with-deps chromium
run:
pnpm --filter @lasvegasfortransit/analytics exec playwright install --with-deps chromium

- name: Verify public sites
run: |
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,27 @@ Analytics stays off on localhost, preview deployments, retired archives, and fra

## Choose an integration

| Project | Entry point | Setup |
| -------------------- | ----------------------- | ---------------------------------------------------------- |
| Astro | `@lvbt/analytics/astro` | Add `lvbtAnalytics({ site })` to `astro.config.ts` |
| Vite or React | `@lvbt/analytics` | Call `init({ site, token })` before rendering |
| React component tree | `@lvbt/analytics/react` | Render `<Analytics>` and call `useTrack()` |
| Plain HTML | generated client | Run `lvbt-analytics client --out public/lvbt-analytics.js` |
| Project | Entry point | Setup |
| -------------------- | ------------------------------------- | ---------------------------------------------------------- |
| Astro | `@lasvegasfortransit/analytics/astro` | Add `lvbtAnalytics({ site })` to `astro.config.ts` |
| Vite or React | `@lasvegasfortransit/analytics` | Call `init({ site, token })` before rendering |
| React component tree | `@lasvegasfortransit/analytics/react` | Render `<Analytics>` and call `useTrack()` |
| Plain HTML | generated client | Run `lvbt-analytics client --out public/lvbt-analytics.js` |

Production builds receive `PUBLIC_LVBT_CWA_TOKEN` and set `LVBT_REQUIRE_ANALYTICS=1`. Preview and
local builds receive neither value, so the client is absent rather than merely pointed at a test
property.

```ts
import lvbtAnalytics from '@lvbt/analytics/astro';
import lvbtAnalytics from '@lasvegasfortransit/analytics/astro';

export default defineConfig({
integrations: [lvbtAnalytics({ site: 'labs.lasvegasfortransit.org' })],
});
```

```ts
import { init } from '@lvbt/analytics';
import { init } from '@lasvegasfortransit/analytics';

init({
site: 'map.lasvegasfortransit.org',
Expand Down
4 changes: 2 additions & 2 deletions apps/collector/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@lvbt/analytics-collector",
"name": "@lasvegasfortransit/analytics-collector",
"version": "0.0.0",
"private": true,
"type": "module",
Expand All @@ -13,7 +13,7 @@
"validate": "wrangler deploy --dry-run --outdir dist"
},
"dependencies": {
"@lvbt/analytics": "workspace:*",
"@lasvegasfortransit/analytics": "workspace:*",
"zod": "catalog:"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion apps/collector/src/columns.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EVENTS, type AnalyticsEvent } from '@lvbt/analytics';
import { EVENTS, type AnalyticsEvent } from '@lasvegasfortransit/analytics';

export function dataPoint(
event: AnalyticsEvent,
Expand Down
2 changes: 1 addition & 1 deletion apps/collector/src/cors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EVENTS } from '@lvbt/analytics';
import { EVENTS } from '@lasvegasfortransit/analytics';

const sites = new Set<string>(Object.values(EVENTS).flatMap((event) => event.sites));

Expand Down
2 changes: 1 addition & 1 deletion apps/collector/src/validate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { eventPayload, EVENTS, type AnalyticsEvent } from '@lvbt/analytics';
import { eventPayload, EVENTS, type AnalyticsEvent } from '@lasvegasfortransit/analytics';
import { z } from 'zod';

export type CollectedEvent = AnalyticsEvent & { country?: string };
Expand Down
7 changes: 4 additions & 3 deletions docs/development/explanation/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ event schema under LVBT control.

## Data paths

Every production site initializes `@lvbt/analytics` with its hostname and the shared Web Analytics
token. The client checks privacy signals, environment, path exclusions, and framing before it does
anything. An enabled client loads Cloudflare's beacon and exposes one typed `track` function.
Every production site initializes `@lasvegasfortransit/analytics` with its hostname and the shared
Web Analytics token. The client checks privacy signals, environment, path exclusions, and framing
before it does anything. An enabled client loads Cloudflare's beacon and exposes one typed `track`
function.

Pageviews, referrers, UTM attribution, and Core Web Vitals go directly to Cloudflare Web Analytics.
Allowlisted conversion events go to `events.lasvegasfortransit.org/e` as small `text/plain` JSON
Expand Down
34 changes: 18 additions & 16 deletions docs/development/reference/api.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Client API

`@lvbt/analytics` has no runtime dependencies. The package exposes a browser client, framework
adapters, browser-test helpers, and a Node helper for static headers.
`@lasvegasfortransit/analytics` has no runtime dependencies. The package exposes a browser client,
framework adapters, browser-test helpers, and a Node helper for static headers.

## `init(options)`

Expand Down Expand Up @@ -65,23 +65,25 @@ by direct `track` calls.

## Framework entry points

`@lvbt/analytics/astro` exports `lvbtAnalytics(options)`. It reads the standard environment, injects
an external page module only when a token exists, and disables JavaScript asset inlining so the site
CSP remains enforceable.
`@lasvegasfortransit/analytics/astro` exports `lvbtAnalytics(options)`. It reads the standard
environment, injects an external page module only when a token exists, and disables JavaScript asset
inlining so the site CSP remains enforceable.

`@lvbt/analytics/react` exports `Analytics`, `useAnalytics`, and `useTrack`. The provider
initializes the shared client after mount. Hooks return the disabled no-op handle during server
rendering and the first client render.
`@lasvegasfortransit/analytics/react` exports `Analytics`, `useAnalytics`, and `useTrack`. The
provider initializes the shared client after mount. Hooks return the disabled no-op handle during
server rendering and the first client render.

`@lvbt/analytics/client` exports `initFromScript`. It reads `data-lvbt-site`, `data-lvbt-token`,
`data-lvbt-collector`, `data-lvbt-spa`, and `data-lvbt-clicks` from a script element.
`@lasvegasfortransit/analytics/client` exports `initFromScript`. It reads `data-lvbt-site`,
`data-lvbt-token`, `data-lvbt-collector`, `data-lvbt-spa`, and `data-lvbt-clicks` from a script
element.

## Test and Node entry points

`@lvbt/analytics/testing` exports `captureBeacon(page)`, `captureEvents(page, collector)`, and
`serveAsProduction(localOrigin, site)`. The capture helpers install Playwright-compatible routes and
return arrays populated as requests occur. `serveAsProduction` returns the production URL and
Chromium host-resolver argument needed to exercise the hostname gate against a loopback server.
`@lasvegasfortransit/analytics/testing` exports `captureBeacon(page)`,
`captureEvents(page, collector)`, and `serveAsProduction(localOrigin, site)`. The capture helpers
install Playwright-compatible routes and return arrays populated as requests occur.
`serveAsProduction` returns the production URL and Chromium host-resolver argument needed to
exercise the hostname gate against a loopback server.

`@lvbt/analytics/node` exports `checkHeadersFile(path, collector)`. It returns every missing CSP
origin from every `Content-Security-Policy` line in a Cloudflare `_headers` file.
`@lasvegasfortransit/analytics/node` exports `checkHeadersFile(path, collector)`. It returns every
missing CSP origin from every `Content-Security-Policy` line in a Cloudflare `_headers` file.
6 changes: 3 additions & 3 deletions docs/development/tutorials/add-analytics-to-a-new-site.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ hostname.
Install the package at the organization-approved version:

```bash
pnpm add @lvbt/analytics
pnpm add @lasvegasfortransit/analytics
```

Use the production hostname as `site`. Do not invent a product ID or include `https://`.

For Astro, add the integration to `astro.config.ts`:

```ts
import lvbtAnalytics from '@lvbt/analytics/astro';
import lvbtAnalytics from '@lasvegasfortransit/analytics/astro';

export default defineConfig({
integrations: [lvbtAnalytics({ site: 'example.lasvegasfortransit.org' })],
Expand All @@ -33,7 +33,7 @@ export default defineConfig({ envPrefix: ['VITE_', 'PUBLIC_'] });

```ts
// src/main.tsx
import { init } from '@lvbt/analytics';
import { init } from '@lasvegasfortransit/analytics';

init({
site: 'example.lasvegasfortransit.org',
Expand Down
6 changes: 3 additions & 3 deletions docs/development/tutorials/start-here.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ published exports, and performs a Wrangler dry run.
Use the focused commands while changing one boundary:

```bash
pnpm --filter @lvbt/analytics test
pnpm --filter @lvbt/analytics-collector test
pnpm --filter @lvbt/analytics-report test
pnpm --filter @lasvegasfortransit/analytics test
pnpm --filter @lasvegasfortransit/analytics-collector test
pnpm --filter @lasvegasfortransit/analytics-report test
```

`pnpm check:fix` applies formatting and safe lint fixes. Run `pnpm check` again after it finishes.
Expand Down
32 changes: 16 additions & 16 deletions docs/superpowers/specs/2026-09-01-analytics-standard-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ Decisions the user made on 2026-09-01:
- Cloudflare Web Analytics is the standard backend. CWA cannot record custom events, so events go to
one tiny first-party Worker writing to Workers Analytics Engine. This is the only way to honor
both choices; it adds one deployable, kept deliberately minimal.
- Distribute the shared client as a published public npm package `@lvbt/analytics` from a new repo
`LasVegasForTransit/analytics`.
- Distribute the shared client as a published public npm package `@lasvegasfortransit/analytics`
from a new repo `LasVegasForTransit/analytics`.
- Wire the standalone `transit-funding` repo now; the Labs copy is canonical going forward and
inherits through the Labs shared hook.
- Heavy emphasis on developer ease of use, turnkey maintenance, and foolproof documentation for a
Expand All @@ -40,7 +40,7 @@ only, never free text, URLs, coordinates, or share ids; no IP or user-agent rete

```
┌──────────────────────────────────────────────────┐
four sites ──────► │ @lvbt/analytics (npm, ~1 KB, zero runtime deps) │
four sites ──────► │ @lasvegasfortransit/analytics (npm, ~1 KB, zero runtime deps) │
init({site,token}) │ gate → inject CWA beacon → track() → clicks │
└───────┬──────────────────────────┬───────────────┘
│ pageviews, CWV, UTM │ allowlisted events
Expand Down Expand Up @@ -69,7 +69,7 @@ Verified facts the design relies on (Cloudflare docs, 2026-09-01):

```
analytics/
├── packages/analytics/ # published as @lvbt/analytics
├── packages/analytics/ # published as @lasvegasfortransit/analytics
│ ├── src/index.ts # init, track, shouldEnable, EVENTS, csp, VERSION, DEFAULT_COLLECTOR
│ ├── src/gate.ts # pure shouldEnable()
│ ├── src/events.ts # THE allowlist (shared with the collector via workspace import)
Expand Down Expand Up @@ -97,7 +97,7 @@ commits are already the org grammar; no per-PR changeset files for volunteers to
runtime dependencies is enforced by a unit test. Size budget: `client.js` ≤ 1024 B gzip, `index.js`
≤ 1536 B gzip (website JS budget is 12 KB with ~10.9 KB used).

### Public API of `@lvbt/analytics`
### Public API of `@lasvegasfortransit/analytics`

```ts
// site is the production hostname. It is both the identifier and the gate rule.
Expand Down Expand Up @@ -284,31 +284,31 @@ Each numbered step is one PR in one repo. Order matters: the package must publis
- `/privacy`: `src/pages/privacy.astro` rendering a vendored copy of
`analytics/docs/public/privacy.md` with a "canonical lives in analytics repo" comment; footer
link; sitemap. Fix `docs/reference/newsletter-ops.md:64-68` (Beehiiv, not Ghost).
- `tests/analytics.spec.ts` in the `ui-contracts` project using `@lvbt/analytics/testing`: served as
production → one beacon request, join click → one `join_click` POST; served from localhost → zero
requests.
- `tests/analytics.spec.ts` in the `ui-contracts` project using
`@lasvegasfortransit/analytics/testing`: served as production → one beacon request, join click →
one `join_click` POST; served from localhost → zero requests.
- Docs: `docs/reference/analytics.md`; shorten the RUM section of
`docs/standards/performance-monitoring.md` to point there; glossary and index entries.
- Measure `pnpm check:baseline`; bump `perf-budgets.json` `jsGzipKb` to 13 only if needed.

### Step 3. Labs

- `pnpm-workspace.yaml` catalog: `'@lvbt/analytics': <exact>`; `turbo.json`
- `pnpm-workspace.yaml` catalog: `'@lasvegasfortransit/analytics': <exact>`; `turbo.json`
`globalEnv: ["PUBLIC_LVBT_CWA_TOKEN", "LVBT_REQUIRE_ANALYTICS"]` (prevents a cached token-less
build being replayed).
- `packages/brand`: first JS exports `./analytics` (`LABS_SITE`, `initLabsAnalytics()` which
dynamically imports `@lvbt/analytics` only when the token is set) and `./analytics/astro`
(`labsAnalytics()` wrapping the integration); `dependencies: { '@lvbt/analytics': 'catalog:' }`;
`env.d.ts`; `tests/analytics.test.ts`. This honors
`docs/development/reference/brand-and-ui.md:55`.
dynamically imports `@lasvegasfortransit/analytics` only when the token is set) and
`./analytics/astro` (`labsAnalytics()` wrapping the integration);
`dependencies: { '@lasvegasfortransit/analytics': 'catalog:' }`; `env.d.ts`;
`tests/analytics.test.ts`. This honors `docs/development/reference/brand-and-ui.md:55`.
- `apps/home/astro.config.ts`: `integrations: [sitemap(), labsAnalytics()]`.
`apps/transit-funding/vite.config.ts`: `envPrefix: ['VITE_', 'PUBLIC_']`; `src/main.tsx`:
`void initLabsAnalytics()` before `createRoot`.
- Archive builds: both `build:archive` scripts run with `PUBLIC_LVBT_CWA_TOKEN=` (empty); new
`tooling/src/check-archive.ts` fails `pnpm check` if any file under `apps/*/dist-archive` contains
`cloudflareinsights` or the collector host; `.env.example` per app.
- `eslint.config.ts`: `no-restricted-imports` for `apps/**` forbidding direct `@lvbt/analytics`
(message: use `@lvbt/brand/analytics`).
- `eslint.config.ts`: `no-restricted-imports` for `apps/**` forbidding direct
`@lasvegasfortransit/analytics` (message: use `@lvbt/brand/analytics`).
- `public/_headers` per app with the standard CSP block (labs docs already require one; separable
into its own PR if it causes churn). The JSON-LD `<script type="application/ld+json">` is data and
passes `script-src 'self'`.
Expand Down Expand Up @@ -364,7 +364,7 @@ Each numbered step is one PR in one repo. Order matters: the package must publis
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Unit (analytics repo) | table-driven `shouldEnable` for every reason; `EVENTS` validation rejects unknown names, extra keys, non-enum values; `csp.merge` idempotent; `init` idempotent under React StrictMode; `dependencies` empty; collector handler tests assert `writeDataPoint` call counts (1 valid, 0 for GPC/400/403/429) |
| Size | `check-size.ts` in the analytics repo; website `bundle-size.ts` 12 KB budget; TransitMapper `report-bundle.ts` |
| Browser (each consumer) | Playwright with `@lvbt/analytics/testing`: production host → exactly one `beacon.min.js` request and one `/e` POST with the exact JSON on the conversion; localhost/preview/archive/framed/GPC → zero requests; no cookies or storage written |
| Browser (each consumer) | Playwright with `@lasvegasfortransit/analytics/testing`: production host → exactly one `beacon.min.js` request and one `/e` POST with the exact JSON on the conversion; localhost/preview/archive/framed/GPC → zero requests; no cookies or storage written |
| Static (each consumer CI) | `lvbt-analytics csp --check public/_headers`; labs `check:archive` grep |
| Deploy smoke | `verify … --expect present` on production, `--expect absent` on website previews and TransitMapper embeds; the Astro integration/shell guard fails the build if the token is missing |
| Live, weekly | `weekly.yml` verifies all four hosts and publishes the report issue |
Expand Down
Loading
Loading