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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ jobs:
- name: Build all packages
run: pnpm build

- name: Validate published Elysia v2 declarations
run: pnpm --filter @logtide/elysia test:declarations

# ====================
# TypeScript Check
# ====================
Expand Down Expand Up @@ -129,6 +132,11 @@ jobs:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.0

- name: Install dependencies
run: pnpm install --frozen-lockfile

Expand All @@ -138,6 +146,9 @@ jobs:
- name: Run smoke tests (Hono, Elysia, Express, Fastify)
run: pnpm test:smoke

- name: Run Elysia v2 WebSocket regression test
run: pnpm --filter test-app-elysia-v2 test:websocket

# ====================
# E2E Tests (Playwright)
# ====================
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ coverage/
.cache/
.temp/
tmp/
.bun-test/

# Framework build output
.next/
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **`@logtide/elysia/next`** — an isolated Elysia 2.x integration using the new `request`, `afterHandle`, and `error` lifecycle contracts. The existing `@logtide/elysia` export remains on the Elysia 1.4 contract.

## [0.12.0] - 2026-08-06

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
| [`@logtide/sveltekit`](./packages/sveltekit) | [![npm](https://img.shields.io/npm/v/@logtide/sveltekit?color=blue)](https://www.npmjs.com/package/@logtide/sveltekit) | SvelteKit hooks integration |
| [`@logtide/hono`](./packages/hono) | [![npm](https://img.shields.io/npm/v/@logtide/hono?color=blue)](https://www.npmjs.com/package/@logtide/hono) | Hono middleware |
| [`@logtide/angular`](./packages/angular) | [![npm](https://img.shields.io/npm/v/@logtide/angular?color=blue)](https://www.npmjs.com/package/@logtide/angular) | Angular ErrorHandler, HTTP Interceptor |
| [`@logtide/elysia`](./packages/elysia) | [![npm](https://img.shields.io/npm/v/@logtide/elysia?color=blue)](https://www.npmjs.com/package/@logtide/elysia) | Elysia plugin |
| [`@logtide/elysia`](./packages/elysia) | [![npm](https://img.shields.io/npm/v/@logtide/elysia?color=blue)](https://www.npmjs.com/package/@logtide/elysia) | Elysia 1.4 plugin + Elysia 2 preview via `/next` |
| [`@logtide/browser`](./packages/browser) | [![npm](https://img.shields.io/npm/v/@logtide/browser?color=blue)](https://www.npmjs.com/package/@logtide/browser) | Browser SDK — Web Vitals, breadcrumbs, offline transport |
| [`@logtide/cli`](./packages/cli) | [![npm](https://img.shields.io/npm/v/@logtide/cli?color=blue)](https://www.npmjs.com/package/@logtide/cli) | CLI — source maps upload and release management |
| [`@logtide/sdk-node`](./packages/node) | [![npm](https://img.shields.io/npm/v/@logtide/sdk-node?color=blue)](https://www.npmjs.com/package/@logtide/sdk-node) | Legacy Node.js SDK _(use `@logtide/express` or `@logtide/fastify` instead)_ |
Expand Down Expand Up @@ -101,7 +101,7 @@ npx @logtide/cli sourcemaps upload --api-url https://your-logtide-instance.com -
├── @logtide/sveltekit ← SvelteKit (handle/handleError/handleFetch)
├── @logtide/hono ← Hono middleware
├── @logtide/angular ← Angular (ErrorHandler + HttpInterceptor)
├── @logtide/elysia ← Elysia plugin
├── @logtide/elysia ← Elysia 1.4 plugin + Elysia 2 /next export
├── @logtide/browser ← Browser SDK (Web Vitals, breadcrumbs, offline)
├── @logtide/cli ← CLI (source maps upload)
└── @logtide/sdk-node ← Legacy standalone Node.js SDK
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"scripts": {
"build": "pnpm -r --filter @logtide/* build",
"test": "pnpm -r --filter @logtide/* test",
"test:smoke": "pnpm -r --filter test-app-hono --filter test-app-elysia --filter test-app-node-express --filter test-app-node-fastify test",
"test:smoke": "pnpm -r --filter test-app-hono --filter test-app-elysia --filter test-app-elysia-v2 --filter test-app-node-express --filter test-app-node-fastify test",
"test:e2e": "pnpm -r --filter test-app-nextjs --filter test-app-sveltekit --filter test-app-nuxt --filter test-app-angular test",
"test:all": "pnpm test && pnpm test:smoke",
"typecheck": "pnpm -r typecheck",
Expand All @@ -19,7 +19,7 @@
"tar": ">=7.5.11",
"esbuild": ">=0.25.0",
"webpack": ">=5.104.1",
"cookie": ">=0.7.0",
"cookie@<0.7.0": ">=0.7.0",
"minimatch": ">=10.2.3",
"rollup": ">=4.59.0",
"ajv": ">=8.18.0",
Expand All @@ -34,7 +34,7 @@
"simple-git": ">=3.32.3",
"@hono/node-server": ">=1.19.10",
"defu": ">=6.1.5",
"elysia": ">=1.4.27",
"elysia@<2": ">=1.4.27 <2",
"h3": ">=1.15.9",
"immutable": ">=5.1.5",
"lodash": ">=4.18.1",
Expand Down
56 changes: 48 additions & 8 deletions packages/elysia/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<p align="center">
<a href="https://www.npmjs.com/package/@logtide/elysia"><img src="https://img.shields.io/npm/v/@logtide/elysia?color=blue" alt="npm"></a>
<a href="../../LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
<a href="https://elysiajs.com/"><img src="https://img.shields.io/badge/Elysia-1+-7C6AEF.svg" alt="Elysia"></a>
<a href="https://elysiajs.com/"><img src="https://img.shields.io/badge/Elysia-1.4%20%7C%202.x-7C6AEF.svg" alt="Elysia"></a>
<a href="https://github.com/logtide-dev/logtide-javascript/releases"><img src="https://img.shields.io/github/v/release/logtide-dev/logtide-javascript" alt="Release"></a>
</p>

Expand All @@ -19,8 +19,8 @@

## Features

- **Automatic request spans** via `onRequest` / `onAfterHandle` lifecycle hooks
- **Error capture** via `onError` hook with full request context
- **Automatic request spans** via version-specific Elysia lifecycle hooks
- **Error capture** with full request context
- **W3C Trace Context** propagation (`traceparent` in/out)
- **Breadcrumbs** for HTTP requests
- **Global plugin** — traces all routes automatically
Expand All @@ -41,6 +41,8 @@ bun add @logtide/elysia

## Quick Start

Use the root export with Elysia 1.x:

```typescript
import { Elysia } from 'elysia';
import { logtide } from '@logtide/elysia';
Expand All @@ -58,17 +60,50 @@ const app = new Elysia()
.listen(3000);
```

Use the isolated `next` export with Elysia 2.x:

```typescript
import { Elysia } from 'elysia';
import { logtide } from '@logtide/elysia/next';

const app = new Elysia()
.use(logtide({
dsn: 'https://lp_your_key@your-instance.com',
service: 'my-elysia-api',
environment: 'production',
}))
.get('/hello', () => 'Hello World')
.listen(3000);
```

Do not mix the root and `next` exports: they intentionally target the incompatible Elysia 1.4 and 2.x contracts respectively.

## Version support

| Elysia version | Import | Support |
| --- | --- | --- |
| 1.0 to 1.4 | `@logtide/elysia` | Supported |
| 2.0.0 prereleases from `beta.4` | `@logtide/elysia/next` | Supported |
| Stable 2.x | `@logtide/elysia/next` | Supported |
| 3.x | — | Not supported |

npm semver can only match a prerelease when a range names its exact `major.minor.patch` tuple. The `2.0.0-beta.4` range therefore accepts later `2.0.0` prereleases, but not a prerelease on a future 2.x patch or minor. We add support for a new prerelease line after validating it.

---

## How It Works

The plugin registers global lifecycle hooks on the Elysia app:
Both plugins register global lifecycle hooks on the Elysia app:

1. **`onRequest`** — extracts incoming `traceparent`, creates a span, stores scope on a WeakMap keyed by the request
2. **`onAfterHandle`** — finishes the span with `ok` or `error`, injects `traceparent` into response headers
3. **`onError`** — finishes the span as `error`, captures the error with full HTTP context
| Stage | Elysia 1.4 (`@logtide/elysia`) | Elysia 2 (`@logtide/elysia/next`) |
|-------|---------------------------------|-------------------------------------|
| Request | `onRequest` | `request` |
| Response | `onAfterHandle` | `afterHandle` + `afterResponse` |
| Error | `onError` | `error` |

The plugin is registered with `.as('global')` so it applies to all routes, including those added after the plugin.
The request hook extracts an incoming `traceparent` and creates a span. The response hooks inject `traceparent` and finish the span using the final mapped status. The error hook captures unexpected and 5xx failures with HTTP context; controlled 4xx responses remain normal request telemetry.

Each plugin is registered with `.as('global')` so it applies to all routes, including those added after the plugin.

---

Expand Down Expand Up @@ -126,8 +161,13 @@ Outgoing responses include a `traceparent` header with the span's context.
## Exports

```typescript
// Elysia 1.4
import { logtide } from '@logtide/elysia';
import type { LogtideElysiaOptions } from '@logtide/elysia';

// Elysia 2.x
import { logtide } from '@logtide/elysia/next';
import type { LogtideElysiaOptions } from '@logtide/elysia/next';
```

---
Expand Down
23 changes: 19 additions & 4 deletions packages/elysia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,25 @@
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./next": {
"import": {
"types": "./dist/next.d.ts",
"default": "./dist/next.js"
},
"require": {
"types": "./dist/next.d.cts",
"default": "./dist/next.cjs"
}
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"build": "tsup && node scripts/patch-next-declarations.mjs",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No CI step semantically validates the patched dist/next.d.ts: skipLibCheck is on repo-wide and the patch test only checks string rewriting, so an unresolvable specifier in the shipped declarations would go unnoticed. A small tsc fixture over the built d.ts (or attw) would catch it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4a45003. test:declarations compiles a consumer fixture that imports the built dist/next.d.ts, then TypeScript's module resolver checks every import, export, and import-type specifier in the ESM and CJS declarations. CI runs it after the build.

"test": "vitest run",
"test:declarations": "tsc --project tests/fixtures/next-declarations-consumer/tsconfig.json && node scripts/validate-next-declarations.mjs",
"typecheck": "tsc --noEmit",
"clean": "node -e \"require('fs').rmSync('dist', {recursive: true, force: true})\""
},
Expand All @@ -46,13 +57,17 @@
"@logtide/types": "workspace:*"
},
"peerDependencies": {
"elysia": ">=1.0.0"
"elysia": ">=1.0.0 <2.0.0 || >=2.0.0-beta.4 <3.0.0-0",
"typebox": ">=1.3.0"
},
"devDependencies": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typebox is a required non-optional peer of elysia@2.0.0-beta.4 but is missing here (and in test-apps/elysia-v2/package.json, which added exact-mirror but not typebox). It currently resolves only via pnpm auto-install-peers; under npm/yarn or with auto-install-peers disabled, elysia 2 type-system use throws Cannot find module 'typebox' at runtime. Please add it explicitly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 852cdd4. typebox >=1.3.0 is now an explicit peer dependency, with typebox ^1.3.15 in the package development setup and smoke fixture.

"@sinclair/typebox": "^0.34.48",
"elysia": "^1.2.0",
"elysia": "npm:elysia@^1.4.29",
"elysia-next": "npm:elysia@2.0.0-beta.4",
"exact-mirror": "^1.2.4",
"file-type": "^22.0.0",
"tsup": "^8.5.1",
"typescript": "^5.5.4"
"typebox": "^1.3.15",
"typescript": "^5.7.0"
}
}
82 changes: 82 additions & 0 deletions packages/elysia/scripts/patch-next-declarations.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { readFile, writeFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
import ts from 'typescript';

const declarationPaths = ['dist/next.d.ts', 'dist/next.d.cts'];
const developmentAlias = 'elysia-next';
const publishedSpecifier = 'elysia';

function isDevelopmentAlias(specifier) {
return specifier === developmentAlias || specifier.startsWith(`${developmentAlias}/`);
}

function aliasReplacement(specifier) {
return specifier === developmentAlias
? publishedSpecifier
: `${publishedSpecifier}${specifier.slice(developmentAlias.length)}`;
}

function aliasSpecifiers(sourceFile) {
const specifiers = [];

function addSpecifier(node) {
if (!node || !ts.isStringLiteral(node) || !isDevelopmentAlias(node.text)) return;

const start = node.getStart(sourceFile) + 1;
const end = node.getEnd() - 1;
specifiers.push({ start, end, replacement: aliasReplacement(node.text) });
}

function visit(node) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visitor misses declare module 'elysia-next' augmentation blocks and triple-slash references, and hasDevelopmentAlias reuses the same visitor, so an unrewritten specifier would publish without failing the build. A raw substring post-check on the patched output would close the shared blind spot (and is cheaper than the second AST parse).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 852cdd4. The declaration patcher now performs one AST rewrite pass and checks the patched output with a raw elysia-next substring post-condition, so missed augmentation or triple-slash occurrences fail the build.

if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
addSpecifier(node.moduleSpecifier);
} else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
addSpecifier(node.argument.literal);
} else if (ts.isExternalModuleReference(node)) {
addSpecifier(node.expression);
}

ts.forEachChild(node, visit);
}

ts.forEachChild(sourceFile, visit);
return specifiers;
}

function patchDeclaration(declaration) {
const sourceFile = ts.createSourceFile('next.d.ts', declaration, ts.ScriptTarget.Latest, false);
const replacements = aliasSpecifiers(sourceFile);

return replacements
.reverse()
.reduce(
(patched, { start, end, replacement }) =>
`${patched.slice(0, start)}${replacement}${patched.slice(end)}`,
declaration
);
}

export async function patchNextDeclarations(paths = declarationPaths) {
const patchedDeclarations = [];

for (const path of paths) {
const declaration = await readFile(path, 'utf8');
const patched = patchDeclaration(declaration);

if (new RegExp(`\\b${developmentAlias}\\b`).test(patched)) {
throw new Error(`Expected ${path} to contain no elysia-next development alias after patching`);
}

patchedDeclarations.push([path, patched, declaration]);
}

for (const [path, patched, declaration] of patchedDeclarations) {
if (patched !== declaration) {
await writeFile(path, patched);
}
}
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
await patchNextDeclarations();
}
55 changes: 55 additions & 0 deletions packages/elysia/scripts/validate-next-declarations.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import ts from 'typescript';

const fixtureConfigPath = 'tests/fixtures/next-declarations-consumer/tsconfig.json';
const declarationPaths = ['dist/next.d.ts', 'dist/next.d.cts'];

function readCompilerOptions(configPath) {
const config = ts.readConfigFile(configPath, ts.sys.readFile);
if (config.error) throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n'));

return ts.parseJsonConfigFileContent(config.config, ts.sys, dirname(configPath)).options;
}

function moduleSpecifiers(sourceFile) {
const specifiers = new Set();

function addSpecifier(node) {
if (node && ts.isStringLiteral(node)) specifiers.add(node.text);
}

function visit(node) {
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
addSpecifier(node.moduleSpecifier);
} else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
addSpecifier(node.argument.literal);
} else if (ts.isExternalModuleReference(node)) {
addSpecifier(node.expression);
}

ts.forEachChild(node, visit);
}

ts.forEachChild(sourceFile, visit);
return specifiers;
}

export function validateNextDeclarations(paths = declarationPaths, fixtureConfig = fixtureConfigPath) {
const compilerOptions = readCompilerOptions(fixtureConfig);
const host = ts.sys;

for (const path of paths) {
const declaration = readFileSync(path, 'utf8');
const sourceFile = ts.createSourceFile(path, declaration, ts.ScriptTarget.Latest, false);

for (const specifier of moduleSpecifiers(sourceFile)) {
const resolved = ts.resolveModuleName(specifier, resolve(path), compilerOptions, host).resolvedModule;
if (!resolved) {
throw new Error(`Unable to resolve ${JSON.stringify(specifier)} from ${path}`);
}
}
}
}

validateNextDeclarations();
1 change: 1 addition & 0 deletions packages/elysia/src/next.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { logtide, type LogtideElysiaOptions } from './plugin-next';
Loading
Loading