Skip to content

Commit 175b101

Browse files
Release v0.4.0 (#27)
* Add onEvent observability hooks to all middleware packages (#18) * Add onEvent observability hooks to all middleware packages Enable logging, metrics, and monitoring by passing an onEvent callback to createDeviceRouter(). Emits profile:classify, profile:store, bot:reject, and error events with timing data. Callbacks are fire-and-forget with built-in error isolation so they never disrupt request handling. * Fix endpoint error event losing sessionToken and handle malformed JSON in Hono Hoist sessionToken before try block in all 4 endpoint handlers so the catch block preserves the generated UUID for new sessions instead of re-reading from the cookie (which is undefined for first requests). Catch malformed JSON in Hono endpoint separately to return 400 instead of falling through to the generic 500 handler. Strengthen onEvent tests: assert event.error object and message in all error tests, assert event.signals in all bot:reject tests. * Add observability example with Grafana dashboard and Prometheus metrics (#20) New examples/observability/ directory with a Docker Compose stack (Express + Redis + Prometheus + Grafana) demonstrating all 6 Prometheus metrics wired to the onEvent hook, including the new hint_total counter. The Grafana dashboard is auto-provisioned with 12 panels across 4 rows. * Add GitHub funding configuration * Add docs for rate limiting, profile versioning, Client Hints compat, and meta-frameworks - New docs/meta-frameworks.md: integration guide for Next.js, Remix, SvelteKit using classifyFromHeaders and deriveHints directly from @device-router/types - Add Client Hints browser compatibility table to getting-started.md - Add rate limiting and profile versioning notes to deployment production checklist - Add threshold versioning callout under Custom Thresholds in getting-started.md - Link meta-frameworks guide from README docs index * Add test for async callback rejection in emitEvent Covers the async () => { throw } case that exercises the result.then(undefined, () => {}) swallowing path. * Document Express default error handler stack trace leak Malformed JSON to the probe endpoint triggers Express's default error handler before DeviceRouter code runs, exposing stack traces. Added a production checklist note pointing to custom error handler docs. * Add npm package metadata (repository, homepage, bugs, keywords) All 7 published packages now include repository, homepage, bugs, keywords, and description fields so npm pages link back to GitHub and are discoverable via search. * Add sideEffects, default export condition, funding, provenance, and community docs - Add sideEffects: false to all packages for tree shaking - Add default condition to exports maps for edge/Bun/Deno compat - Add funding field linking to GitHub Sponsors - Add provenance: true to publishConfig and --provenance to publish workflow - Add browser field and ./browser export alias to probe package - Add SECURITY.md with vulnerability reporting policy - Add CODE_OF_CONDUCT.md (Contributor Covenant 2.1) * Fix DeviceProfile.signals type to reflect stored shape (#23) Middleware endpoints strip userAgent and viewport before storing, but DeviceProfile.signals was typed as RawSignals — making TypeScript claim those fields might exist when they never will. Add StoredSignals type alias (Omit<RawSignals, 'userAgent' | 'viewport'>) and use it instead. * Bump the dev-deps group with 2 updates (#21) Bumps the dev-deps group with 2 updates: [eslint](https://github.com/eslint/eslint) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint). Updates `eslint` from 10.0.1 to 10.0.2 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](eslint/eslint@v10.0.1...v10.0.2) Updates `typescript-eslint` from 8.56.0 to 8.56.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/typescript-eslint) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.0.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: typescript-eslint dependency-version: 8.56.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump hono from 4.12.1 to 4.12.2 in the prod-deps group (#22) Bumps the prod-deps group with 1 update: [hono](https://github.com/honojs/hono). Updates `hono` from 4.12.1 to 4.12.2 - [Release notes](https://github.com/honojs/hono/releases) - [Commits](honojs/hono@v4.12.1...v4.12.2) --- updated-dependencies: - dependency-name: hono dependency-version: 4.12.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Align Fastify middleware API shape with Express, Hono, and Koa (#24) createDeviceRouter() now returns { middleware, probeEndpoint, injectionMiddleware? } instead of { plugin, pluginOptions, probeEndpoint, injectionHook? }. Removes the unused pluginOptions (always {}) from the public API. Renames createInjectionHook to createInjectionMiddleware for consistency. * Add server-side diagnostic warnings for missing probe data (#25) * Fix npm badge link to point to package page instead of search * Add server-side diagnostic warnings for missing probe data Surface misconfigurations server-side since the probe client has no room for error reporting within its 1 KB budget. Three strategies, all gated behind NODE_ENV !== 'production': - Startup: log the expected probe endpoint path via console.info - Runtime: one-time console.warn after 50 middleware hits with zero probe submissions - onEvent: emit structured diagnostic:no-probe-data event through the existing onEvent callback * Verify dist artifacts exist before npm publish Prevents publishing empty packages if the build silently fails to emit .js or .d.ts files. * Document threshold immutability constraint in deployment guide Expand the brief production checklist mention into a dedicated section with four mitigation strategies: shorten TTL, flush storage, rotate cookie name, or do nothing when only numeric boundaries change. * Bump v0.4.0 — fix misleading threshold staleness docs Classification runs on every request using current thresholds, so threshold changes take effect immediately. Removed incorrect flush/TTL/cookie-rotation mitigation strategies from deployment guide. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 76701fb commit 175b101

73 files changed

Lines changed: 4432 additions & 403 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/FUNDING.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github: SimplyLiz
2+
buy_me_a_coffee: simplyliz

.github/workflows/publish.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ jobs:
3333

3434
- run: pnpm format:check
3535

36+
- name: Verify dist artifacts exist
37+
run: |
38+
for pkg in types probe storage middleware-express middleware-fastify middleware-hono middleware-koa; do
39+
if ! ls packages/$pkg/dist/*.js &>/dev/null || ! ls packages/$pkg/dist/*.d.ts &>/dev/null; then
40+
echo "Missing dist artifacts in packages/$pkg"
41+
exit 1
42+
fi
43+
done
44+
3645
- name: Validate version matches tag
3746
run: |
3847
TAG_VERSION="${GITHUB_REF_NAME#v}"
@@ -42,7 +51,7 @@ jobs:
4251
exit 1
4352
fi
4453
45-
- run: pnpm -r publish --no-git-checks --access public
54+
- run: pnpm -r publish --no-git-checks --access public --provenance
4655
env:
4756
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
4857

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
## Unreleased
44

5+
## 0.4.0 (2026-02-24)
6+
7+
### Features
8+
9+
- **Observability hooks** — New `onEvent` callback option on all middleware packages. Emits `profile:classify`, `profile:store`, `bot:reject`, and `error` events for plugging in logging, metrics, and monitoring without middleware wrapping. Callbacks are fire-and-forget with built-in error isolation
10+
11+
### Documentation
12+
13+
- **Observability example** — New `examples/observability/` with Docker Compose stack (Express + Redis + Prometheus + Grafana), all 6 Prometheus metrics wired to `onEvent`, and a pre-built Grafana dashboard. Adds `device_router_hint_total` metric for tracking hint activation rates
14+
- **Meta-framework integration guide** — New `docs/meta-frameworks.md` covering Next.js (App Router), Remix, and SvelteKit integration using `classifyFromHeaders` and `deriveHints` directly from `@device-router/types`
15+
- **Client Hints browser compatibility** — Added browser compatibility table to the Getting Started guide documenting which Client Hints headers are available on Chrome, Edge, Safari, and Firefox, and what happens when headers are missing
16+
- **Rate limiting** — Added production checklist note that the probe endpoint has no built-in rate limiting and should be protected via reverse proxy or framework-level rate limiter
17+
- **Threshold staleness fix** — Corrected deployment guide: threshold changes take effect immediately since classification runs on every request. Removed misleading flush/TTL/cookie-rotation mitigation strategies
18+
519
## 0.3.0 (2026-02-23)
620

721
### Breaking Changes

CODE_OF_CONDUCT.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Code of Conduct
2+
3+
## Our Pledge
4+
5+
We are committed to making participation in this project a welcoming experience for everyone,
6+
regardless of background or identity.
7+
8+
## Our Standards
9+
10+
Examples of positive behavior:
11+
12+
- Using welcoming and inclusive language
13+
- Being respectful of differing viewpoints and experiences
14+
- Gracefully accepting constructive feedback
15+
- Focusing on what is best for the community
16+
17+
Examples of unacceptable behavior:
18+
19+
- Trolling, insulting or derogatory comments, and personal attacks
20+
- Publishing others' private information without explicit permission
21+
- Other conduct which could reasonably be considered inappropriate in a professional setting
22+
23+
## Enforcement
24+
25+
Project maintainers are responsible for clarifying standards of acceptable behavior and will take
26+
appropriate action in response to unacceptable behavior. Instances of unacceptable behavior may be
27+
reported via the contact information on the [GitHub profile](https://github.com/SimplyLiz).
28+
29+
## Attribution
30+
31+
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
32+
version 2.1.

README.md

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# DeviceRouter
22

3-
[![npm](https://img.shields.io/npm/v/@device-router/types?label=npm&color=cb3837)](https://www.npmjs.com/search?q=%40device-router)
3+
[![npm](https://img.shields.io/npm/v/@device-router/types?label=npm&color=cb3837)](https://www.npmjs.com/package/@device-router/types)
44
[![CI](https://img.shields.io/github/actions/workflow/status/SimplyLiz/DeviceRouter/ci.yml?branch=main&label=CI)](https://github.com/SimplyLiz/DeviceRouter/actions/workflows/ci.yml)
55
[![bundle size](https://img.shields.io/badge/probe-~1%20KB%20gzipped-blue)](https://github.com/SimplyLiz/DeviceRouter/tree/main/packages/probe)
66
[![license](https://img.shields.io/github/license/SimplyLiz/DeviceRouter)](https://github.com/SimplyLiz/DeviceRouter/blob/main/LICENSE)
@@ -175,6 +175,23 @@ When `classifyFromHeaders` is enabled, mobile/tablet/desktop detection happens f
175175

176176
See [Getting Started — First-Request Handling](docs/getting-started.md#first-request-handling) for details.
177177

178+
## Observability
179+
180+
Plug in logging and metrics with a single callback — no middleware wrapping needed:
181+
182+
```typescript
183+
const { middleware, probeEndpoint } = createDeviceRouter({
184+
storage,
185+
onEvent: (event) => {
186+
if (event.type === 'profile:classify') {
187+
logger.info('classified', { source: event.source, cpu: event.tiers.cpu });
188+
}
189+
},
190+
});
191+
```
192+
193+
Events: `profile:classify`, `profile:store`, `bot:reject`, `error`. See the [Observability guide](docs/observability.md).
194+
178195
## Packages
179196

180197
| Package | Description | Size |
@@ -220,17 +237,20 @@ pnpm dev
220237

221238
Open http://localhost:3000 — the probe runs on first load, refresh to see your detected profile. Use `?force=lite` or `?force=full` to preview each mode without a real device.
222239

223-
| Framework | Guide | Example |
224-
| --------- | ----------------------------------------------------------- | ---------------------------------------- |
225-
| Express | [Quick Start](docs/getting-started.md#quick-start--express) | [express-basic](examples/express-basic/) |
226-
| Fastify | [Quick Start](docs/getting-started.md#quick-start--fastify) | [fastify-basic](examples/fastify-basic/) |
227-
| Hono | [Quick Start](docs/getting-started.md#quick-start--hono) | [hono-basic](examples/hono-basic/) |
228-
| Koa | [Quick Start](docs/getting-started.md#quick-start--koa) | [koa-basic](examples/koa-basic/) |
240+
| Framework | Guide | Example |
241+
| ------------- | ----------------------------------------------------------- | ---------------------------------------- |
242+
| Express | [Quick Start](docs/getting-started.md#quick-start--express) | [express-basic](examples/express-basic/) |
243+
| Fastify | [Quick Start](docs/getting-started.md#quick-start--fastify) | [fastify-basic](examples/fastify-basic/) |
244+
| Hono | [Quick Start](docs/getting-started.md#quick-start--hono) | [hono-basic](examples/hono-basic/) |
245+
| Koa | [Quick Start](docs/getting-started.md#quick-start--koa) | [koa-basic](examples/koa-basic/) |
246+
| Observability | [Observability guide](docs/observability.md) | [observability](examples/observability/) |
229247

230248
## Documentation
231249

232250
- [Getting Started](docs/getting-started.md)
251+
- [Observability](docs/observability.md) — Logging, metrics, and monitoring hooks
233252
- [Deployment Guide](docs/deployment.md) — Docker, Cloudflare Workers, serverless
253+
- [Meta-Framework Integration](docs/meta-frameworks.md) — Next.js, Remix, SvelteKit
234254
- [Profile Schema Reference](docs/profile-schema.md)
235255
- API Reference: [types](docs/api/types.md) | [probe](docs/api/probe.md) | [storage](docs/api/storage.md) | [express](docs/api/middleware-express.md) | [fastify](docs/api/middleware-fastify.md) | [hono](docs/api/middleware-hono.md) | [koa](docs/api/middleware-koa.md)
236256

SECURITY.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Security Policy
2+
3+
## Supported Versions
4+
5+
| Version | Supported |
6+
| ------- | --------- |
7+
| 0.3.x | Yes |
8+
| < 0.3 | No |
9+
10+
## Reporting a Vulnerability
11+
12+
If you discover a security issue, please report it responsibly.
13+
14+
**Do not open a public GitHub issue for security vulnerabilities.**
15+
16+
Instead, please email security concerns to the maintainers via the contact information on the
17+
[GitHub profile](https://github.com/SimplyLiz). Include:
18+
19+
- A description of the vulnerability
20+
- Steps to reproduce
21+
- Potential impact
22+
- Suggested fix (if any)
23+
24+
You can expect an initial response within 72 hours. We will work with you to understand the issue
25+
and coordinate a fix before any public disclosure.
26+
27+
## Scope
28+
29+
This policy applies to all packages in the `@device-router/*` scope published to npm, as well as
30+
the source code in this repository.

docs/api/middleware-express.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,21 @@ app.use(middleware);
2121

2222
### Options
2323

24-
| Option | Type | Default | Description |
25-
| --------------------- | -------------------------------------- | ------------------------ | ------------------------------------------------------------ |
26-
| `storage` | `StorageAdapter` | required | Storage backend |
27-
| `cookieName` | `string` | `'dr_session'` | Session cookie name |
28-
| `cookiePath` | `string` | `'/'` | Cookie path |
29-
| `cookieSecure` | `boolean` | `false` | Set `Secure` flag on the session cookie |
30-
| `ttl` | `number` | `86400` | Profile TTL in seconds |
31-
| `rejectBots` | `boolean` | `true` | Reject bot/crawler probe submissions (returns 403) |
32-
| `thresholds` | `TierThresholds` | built-in defaults | Custom tier classification thresholds (validated at startup) |
33-
| `injectProbe` | `boolean` | `false` | Auto-inject probe script into HTML responses |
34-
| `probePath` | `string` | `'/device-router/probe'` | Custom probe endpoint path for injected script |
35-
| `probeNonce` | `string \| ((req: Request) => string)` || CSP nonce for the injected script tag |
36-
| `fallbackProfile` | `FallbackProfile` || Fallback profile for first requests without probe data |
37-
| `classifyFromHeaders` | `boolean` | `false` | Classify from UA/Client Hints on first request |
24+
| Option | Type | Default | Description |
25+
| --------------------- | -------------------------------------- | ------------------------ | ------------------------------------------------------------------------- |
26+
| `storage` | `StorageAdapter` | required | Storage backend |
27+
| `cookieName` | `string` | `'dr_session'` | Session cookie name |
28+
| `cookiePath` | `string` | `'/'` | Cookie path |
29+
| `cookieSecure` | `boolean` | `false` | Set `Secure` flag on the session cookie |
30+
| `ttl` | `number` | `86400` | Profile TTL in seconds |
31+
| `rejectBots` | `boolean` | `true` | Reject bot/crawler probe submissions (returns 403) |
32+
| `thresholds` | `TierThresholds` | built-in defaults | Custom tier classification thresholds (validated at startup) |
33+
| `injectProbe` | `boolean` | `false` | Auto-inject probe script into HTML responses |
34+
| `probePath` | `string` | `'/device-router/probe'` | Custom probe endpoint path for injected script |
35+
| `probeNonce` | `string \| ((req: Request) => string)` || CSP nonce for the injected script tag |
36+
| `fallbackProfile` | `FallbackProfile` || Fallback profile for first requests without probe data |
37+
| `classifyFromHeaders` | `boolean` | `false` | Classify from UA/Client Hints on first request |
38+
| `onEvent` | `OnEventCallback` || Observability callback for logging/metrics ([guide](../observability.md)) |
3839

3940
### Returns
4041

0 commit comments

Comments
 (0)