From 2888c02199f64e877281f302b03d063c6cf99f89 Mon Sep 17 00:00:00 2001 From: Polliog <40077351+Polliog@users.noreply.github.com> Date: Sun, 28 Jun 2026 12:07:32 +0200 Subject: [PATCH] feat(browser): export initLogtide; dedup framework client init Add `initLogtide` (and the `buildBrowserIntegrations` / `buildBrowserTransportWrapper` helpers) to `@logtide/browser`, matching the React integration docs (`import { initLogtide } from '@logtide/browser'`). The Next.js, SvelteKit and Angular client init now delegate to the shared `initLogtide`, removing the duplicated browser-init logic. SvelteKit now defaults `service` to 'sveltekit' (was 'unknown'), aligning it with Next.js and Angular. Bump all packages to 0.10.0. Fixes #9 --- CHANGELOG.md | 12 +++ package.json | 2 +- packages/angular/package.json | 2 +- packages/angular/src/provide.ts | 80 +---------------- packages/browser/package.json | 2 +- packages/browser/src/index.ts | 8 ++ packages/browser/src/init.ts | 114 +++++++++++++++++++++++++ packages/browser/tests/init.test.ts | 51 +++++++++++ packages/cli/package.json | 2 +- packages/core/package.json | 2 +- packages/elysia/package.json | 2 +- packages/express/package.json | 2 +- packages/fastify/package.json | 2 +- packages/hono/package.json | 2 +- packages/nextjs/package.json | 2 +- packages/nextjs/src/client/index.ts | 61 +------------ packages/node/package.json | 2 +- packages/nuxt/package.json | 2 +- packages/sveltekit/package.json | 2 +- packages/sveltekit/src/client/index.ts | 60 +------------ packages/types/package.json | 2 +- 21 files changed, 206 insertions(+), 208 deletions(-) create mode 100644 packages/browser/src/init.ts create mode 100644 packages/browser/tests/init.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 487bb59..97b24f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ 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). +## [0.10.0] - 2026-06-28 + +### Added + +- **`@logtide/browser`: `initLogtide` export** — the browser SDK now exposes `initLogtide(options)` directly, matching the React integration docs (`import { initLogtide } from '@logtide/browser'`). It wires up the global error handler, the default browser integrations (click/network breadcrumbs, optional Web Vitals) and offline resilience, then binds the session id to the global scope. Resolves [#9](https://github.com/logtide-dev/logtide-javascript/issues/9). +- **`@logtide/browser`: `buildBrowserIntegrations` / `buildBrowserTransportWrapper` helpers** — exported building blocks used by `initLogtide` and the framework wrappers. + +### Changed + +- **Framework client init now delegates to `@logtide/browser`** (`@logtide/nextjs`, `@logtide/sveltekit`, `@logtide/angular`): the previously duplicated browser-init logic in each package is gone — all three call the shared `initLogtide`, passing their own `defaultService`. Behaviour is unchanged except for the SvelteKit default below. +- **`@logtide/sveltekit`: default service is now `'sveltekit'`** when `service` is not provided in the options. Previously the SvelteKit client init set no default, so logs fell back to `'unknown'`; it now matches the Next.js (`'nextjs'`) and Angular (`'angular'`) behaviour. An explicit `service` still wins. + ## [0.9.0] - 2026-06-22 ### Added diff --git a/package.json b/package.json index 8e35969..e6c71c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "0.9.0", + "version": "0.10.0", "scripts": { "build": "pnpm -r --filter @logtide/* build", "test": "pnpm -r --filter @logtide/* test", diff --git a/packages/angular/package.json b/packages/angular/package.json index fd7dd06..980c93d 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@logtide/angular", - "version": "0.9.0", + "version": "0.10.0", "description": "LogTide SDK integration for Angular — ErrorHandler, HTTP Interceptor, trace propagation", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/angular/src/provide.ts b/packages/angular/src/provide.ts index f12be6e..d5337f7 100644 --- a/packages/angular/src/provide.ts +++ b/packages/angular/src/provide.ts @@ -6,64 +6,10 @@ import { APP_INITIALIZER, } from '@angular/core'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; -import type { Integration, Transport } from '@logtide/types'; -import { hub, GlobalErrorIntegration, resolveDSN } from '@logtide/core'; -import { - getSessionId, - WebVitalsIntegration, - ClickBreadcrumbIntegration, - NetworkBreadcrumbIntegration, - OfflineTransport, - type BrowserClientOptions, -} from '@logtide/browser'; +import { initLogtide, type BrowserClientOptions } from '@logtide/browser'; import { LogtideErrorHandler } from './error-handler'; import { LogtideHttpInterceptor } from './http-interceptor'; -function buildBrowserIntegrations(options: BrowserClientOptions): Integration[] { - const browserOpts = options.browser ?? {}; - const integrations: Integration[] = []; - const apiUrl = resolveDSN(options).apiUrl; - - if (browserOpts.webVitals) { - integrations.push( - new WebVitalsIntegration({ - sampleRate: browserOpts.webVitalsSampleRate, - }), - ); - } - - if (browserOpts.clickBreadcrumbs !== false) { - const clickOpts = typeof browserOpts.clickBreadcrumbs === 'object' - ? browserOpts.clickBreadcrumbs - : undefined; - integrations.push(new ClickBreadcrumbIntegration(clickOpts)); - } - - if (browserOpts.networkBreadcrumbs !== false) { - const netOpts = typeof browserOpts.networkBreadcrumbs === 'object' - ? browserOpts.networkBreadcrumbs - : {}; - integrations.push( - new NetworkBreadcrumbIntegration({ ...netOpts, apiUrl }), - ); - } - - return integrations; -} - -function buildTransportWrapper(options: BrowserClientOptions): ((inner: Transport) => Transport) | undefined { - const browserOpts = options.browser ?? {}; - if (browserOpts.offlineResilience === false) return undefined; - - const dsn = resolveDSN(options); - return (inner: Transport) => new OfflineTransport({ - inner, - beaconUrl: `${dsn.apiUrl}/api/v1/ingest`, - apiKey: dsn.apiKey, - debug: options.debug, - }); -} - /** * Provide LogTide in a standalone Angular app (Angular 17+). * @@ -85,17 +31,7 @@ export function provideLogtide(options: BrowserClientOptions): EnvironmentProvid provide: APP_INITIALIZER, useFactory: () => { return () => { - hub.init({ - service: 'angular', - ...options, - transportWrapper: buildTransportWrapper(options) ?? options.transportWrapper, - integrations: [ - new GlobalErrorIntegration(), - ...buildBrowserIntegrations(options), - ...(options.integrations ?? []), - ], - }); - hub.getScope().setSessionId(getSessionId()); + initLogtide(options, { defaultService: 'angular' }); }; }, multi: true, @@ -128,17 +64,7 @@ export function getLogtideProviders(options: BrowserClientOptions): Provider[] { provide: APP_INITIALIZER, useFactory: () => { return () => { - hub.init({ - service: 'angular', - ...options, - transportWrapper: buildTransportWrapper(options) ?? options.transportWrapper, - integrations: [ - new GlobalErrorIntegration(), - ...buildBrowserIntegrations(options), - ...(options.integrations ?? []), - ], - }); - hub.getScope().setSessionId(getSessionId()); + initLogtide(options, { defaultService: 'angular' }); }; }, multi: true, diff --git a/packages/browser/package.json b/packages/browser/package.json index 45a6b9a..c5dd070 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@logtide/browser", - "version": "0.9.0", + "version": "0.10.0", "description": "Logtide browser SDK — Web Vitals, breadcrumbs, session context, offline resilience", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index a76a043..040b34f 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -1,3 +1,11 @@ +// Initialization +export { + initLogtide, + buildBrowserIntegrations, + buildBrowserTransportWrapper, + type InitLogtideExtraOptions, +} from './init'; + // Session export { getSessionId, resetSessionId } from './session'; diff --git a/packages/browser/src/init.ts b/packages/browser/src/init.ts new file mode 100644 index 0000000..6f5c4bd --- /dev/null +++ b/packages/browser/src/init.ts @@ -0,0 +1,114 @@ +import type { Integration, Transport } from '@logtide/types'; +import { hub, GlobalErrorIntegration, resolveDSN } from '@logtide/core'; +import { getSessionId } from './session'; +import { WebVitalsIntegration } from './integrations/web-vitals'; +import { ClickBreadcrumbIntegration } from './integrations/click-breadcrumbs'; +import { NetworkBreadcrumbIntegration } from './integrations/network-breadcrumbs'; +import { OfflineTransport } from './transport/offline-transport'; +import type { BrowserClientOptions } from './types'; + +/** + * Build the default browser integrations from the given options. + * + * Web Vitals are opt-in; click and network breadcrumbs are on by default and + * can be disabled (or configured) via `options.browser`. + */ +export function buildBrowserIntegrations(options: BrowserClientOptions): Integration[] { + const browserOpts = options.browser ?? {}; + const integrations: Integration[] = []; + const apiUrl = resolveDSN(options).apiUrl; + + if (browserOpts.webVitals) { + integrations.push( + new WebVitalsIntegration({ + sampleRate: browserOpts.webVitalsSampleRate, + }), + ); + } + + if (browserOpts.clickBreadcrumbs !== false) { + const clickOpts = typeof browserOpts.clickBreadcrumbs === 'object' + ? browserOpts.clickBreadcrumbs + : undefined; + integrations.push(new ClickBreadcrumbIntegration(clickOpts)); + } + + if (browserOpts.networkBreadcrumbs !== false) { + const netOpts = typeof browserOpts.networkBreadcrumbs === 'object' + ? browserOpts.networkBreadcrumbs + : {}; + integrations.push( + new NetworkBreadcrumbIntegration({ ...netOpts, apiUrl }), + ); + } + + return integrations; +} + +/** + * Build the offline-resilience transport wrapper from the given options. + * + * Returns `undefined` when offline resilience is disabled via + * `options.browser.offlineResilience === false`. + */ +export function buildBrowserTransportWrapper( + options: BrowserClientOptions, +): ((inner: Transport) => Transport) | undefined { + const browserOpts = options.browser ?? {}; + if (browserOpts.offlineResilience === false) return undefined; + + const dsn = resolveDSN(options); + return (inner: Transport) => new OfflineTransport({ + inner, + beaconUrl: `${dsn.apiUrl}/api/v1/ingest`, + apiKey: dsn.apiKey, + debug: options.debug, + }); +} + +export interface InitLogtideExtraOptions { + /** + * Service name to use when `options.service` is not set. Framework wrappers + * pass their own name (e.g. 'nextjs'); an explicit `options.service` always + * wins. + */ + defaultService?: string; +} + +/** + * Initialize LogTide in the browser. + * + * Wires up the global error handler, the default browser integrations + * (click/network breadcrumbs, optional Web Vitals) and offline resilience, + * then binds the session id to the global scope. + * + * @example + * ```ts + * // main.tsx / index.tsx + * import { initLogtide } from '@logtide/browser'; + * + * initLogtide({ + * dsn: 'https://lp_key@api.logtide.dev/proj', + * service: 'react-frontend', + * environment: 'production', + * release: '1.0.0', + * }); + * ``` + */ +export function initLogtide( + options: BrowserClientOptions, + extra: InitLogtideExtraOptions = {}, +): void { + hub.init({ + ...(extra.defaultService ? { service: extra.defaultService } : {}), + ...options, + transportWrapper: buildBrowserTransportWrapper(options) ?? options.transportWrapper, + integrations: [ + new GlobalErrorIntegration(), + ...buildBrowserIntegrations(options), + ...(options.integrations ?? []), + ], + }); + + hub.getScope().setSessionId(getSessionId()); +} diff --git a/packages/browser/tests/init.test.ts b/packages/browser/tests/init.test.ts new file mode 100644 index 0000000..40b9e85 --- /dev/null +++ b/packages/browser/tests/init.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import type { Integration } from '@logtide/types'; +import { hub } from '@logtide/core'; +import { initLogtide } from '../src/init'; + +const DSN = 'https://lp_key@api.logtide.dev/proj'; + +describe('initLogtide', () => { + beforeEach(async () => { + await hub.close(); + }); + + afterEach(async () => { + await hub.close(); + }); + + it('initializes the hub client with the given service', () => { + initLogtide({ dsn: DSN, service: 'react-frontend' }); + + const client = hub.getClient(); + expect(client).not.toBeNull(); + expect(client?.service).toBe('react-frontend'); + }); + + it('sets a session id on the global scope', () => { + initLogtide({ dsn: DSN, service: 'react-frontend' }); + + expect(hub.getScope().sessionId).toBeDefined(); + }); + + it('installs user-provided integrations', () => { + const setup = vi.fn(); + const custom: Integration = { name: 'custom-test', setup }; + + initLogtide({ dsn: DSN, service: 'react-frontend', integrations: [custom] }); + + expect(setup).toHaveBeenCalledOnce(); + }); + + it('applies a default service when none is provided in options', () => { + initLogtide({ dsn: DSN }, { defaultService: 'nextjs' }); + + expect(hub.getClient()?.service).toBe('nextjs'); + }); + + it('prefers the explicit service over the default', () => { + initLogtide({ dsn: DSN, service: 'my-app' }, { defaultService: 'nextjs' }); + + expect(hub.getClient()?.service).toBe('my-app'); + }); +}); diff --git a/packages/cli/package.json b/packages/cli/package.json index e17bc59..a05c01c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@logtide/cli", - "version": "0.9.0", + "version": "0.10.0", "description": "LogTide CLI — upload source maps and manage releases", "type": "module", "bin": { diff --git a/packages/core/package.json b/packages/core/package.json index 86866c8..6fb8bdc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@logtide/core", - "version": "0.9.0", + "version": "0.10.0", "description": "Core client, hub, scope, transports, and utilities for the LogTide SDK", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/elysia/package.json b/packages/elysia/package.json index 713e534..f53bd21 100644 --- a/packages/elysia/package.json +++ b/packages/elysia/package.json @@ -1,6 +1,6 @@ { "name": "@logtide/elysia", - "version": "0.9.0", + "version": "0.10.0", "description": "LogTide SDK plugin for Elysia — request tracing and error capture via lifecycle hooks", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/express/package.json b/packages/express/package.json index ae943c5..d4d4cee 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -1,6 +1,6 @@ { "name": "@logtide/express", - "version": "0.9.0", + "version": "0.10.0", "description": "LogTide SDK middleware for Express — request tracing and error capture", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/fastify/package.json b/packages/fastify/package.json index 42129eb..7461264 100644 --- a/packages/fastify/package.json +++ b/packages/fastify/package.json @@ -1,6 +1,6 @@ { "name": "@logtide/fastify", - "version": "0.9.0", + "version": "0.10.0", "description": "LogTide SDK plugin for Fastify — request tracing and error capture", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/hono/package.json b/packages/hono/package.json index 66ae369..8e773d5 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -1,6 +1,6 @@ { "name": "@logtide/hono", - "version": "0.9.0", + "version": "0.10.0", "description": "LogTide SDK middleware for Hono — request tracing and error capture", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index e50f97a..f4c0391 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@logtide/nextjs", - "version": "0.9.0", + "version": "0.10.0", "description": "LogTide SDK integration for Next.js — auto error capture, request tracing, and performance spans", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 6d0efcf..3800fa3 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -1,13 +1,4 @@ -import type { Integration, Transport } from '@logtide/types'; -import { hub, GlobalErrorIntegration, resolveDSN } from '@logtide/core'; -import { - getSessionId, - WebVitalsIntegration, - ClickBreadcrumbIntegration, - NetworkBreadcrumbIntegration, - OfflineTransport, - type BrowserClientOptions, -} from '@logtide/browser'; +import { initLogtide as initBrowserLogtide, type BrowserClientOptions } from '@logtide/browser'; export { LogtideErrorBoundary } from './error-boundary'; export { trackNavigation } from './navigation'; @@ -24,53 +15,5 @@ export { trackNavigation } from './navigation'; * ``` */ export function initLogtide(options: BrowserClientOptions): void { - const browserOpts = options.browser ?? {}; - const browserIntegrations: Integration[] = []; - const apiUrl = resolveDSN(options).apiUrl; - - if (browserOpts.webVitals) { - browserIntegrations.push( - new WebVitalsIntegration({ - sampleRate: browserOpts.webVitalsSampleRate, - }), - ); - } - - if (browserOpts.clickBreadcrumbs !== false) { - const clickOpts = typeof browserOpts.clickBreadcrumbs === 'object' - ? browserOpts.clickBreadcrumbs - : undefined; - browserIntegrations.push(new ClickBreadcrumbIntegration(clickOpts)); - } - - if (browserOpts.networkBreadcrumbs !== false) { - const netOpts = typeof browserOpts.networkBreadcrumbs === 'object' - ? browserOpts.networkBreadcrumbs - : {}; - browserIntegrations.push( - new NetworkBreadcrumbIntegration({ ...netOpts, apiUrl }), - ); - } - - const transportWrapper = browserOpts.offlineResilience !== false - ? (inner: Transport) => new OfflineTransport({ - inner, - beaconUrl: `${apiUrl}/api/v1/ingest`, - apiKey: resolveDSN(options).apiKey, - debug: options.debug, - }) - : undefined; - - hub.init({ - service: 'nextjs', - ...options, - transportWrapper: transportWrapper ?? options.transportWrapper, - integrations: [ - new GlobalErrorIntegration(), - ...browserIntegrations, - ...(options.integrations ?? []), - ], - }); - - hub.getScope().setSessionId(getSessionId()); + initBrowserLogtide(options, { defaultService: 'nextjs' }); } diff --git a/packages/node/package.json b/packages/node/package.json index fcb9375..3adfa2c 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@logtide/sdk-node", - "version": "0.9.0", + "version": "0.10.0", "description": "Official Node.js SDK for LogTide (logtide.dev) - Self-hosted log management with advanced features: retry logic, circuit breaker, query API, live streaming, and middleware support", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 57b980e..10c8923 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "@logtide/nuxt", - "version": "0.9.0", + "version": "0.10.0", "description": "LogTide SDK integration for Nuxt — auto error capture, request tracing via Nitro hooks", "type": "module", "main": "./dist/module.cjs", diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 2c7afc0..500cbe8 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@logtide/sveltekit", - "version": "0.9.0", + "version": "0.10.0", "description": "LogTide SDK integration for SvelteKit — handle, handleError, handleFetch hooks", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/sveltekit/src/client/index.ts b/packages/sveltekit/src/client/index.ts index 37d9e12..c5028a2 100644 --- a/packages/sveltekit/src/client/index.ts +++ b/packages/sveltekit/src/client/index.ts @@ -1,15 +1,6 @@ export { createBoundaryHandler } from './error-boundary'; -import type { Integration, Transport } from '@logtide/types'; -import { hub, GlobalErrorIntegration, resolveDSN } from '@logtide/core'; -import { - getSessionId, - WebVitalsIntegration, - ClickBreadcrumbIntegration, - NetworkBreadcrumbIntegration, - OfflineTransport, - type BrowserClientOptions, -} from '@logtide/browser'; +import { initLogtide as initBrowserLogtide, type BrowserClientOptions } from '@logtide/browser'; /** * Initialize LogTide on the SvelteKit client side. @@ -22,52 +13,5 @@ import { * ``` */ export function initLogtide(options: BrowserClientOptions): void { - const browserOpts = options.browser ?? {}; - const browserIntegrations: Integration[] = []; - const apiUrl = resolveDSN(options).apiUrl; - - if (browserOpts.webVitals) { - browserIntegrations.push( - new WebVitalsIntegration({ - sampleRate: browserOpts.webVitalsSampleRate, - }), - ); - } - - if (browserOpts.clickBreadcrumbs !== false) { - const clickOpts = typeof browserOpts.clickBreadcrumbs === 'object' - ? browserOpts.clickBreadcrumbs - : undefined; - browserIntegrations.push(new ClickBreadcrumbIntegration(clickOpts)); - } - - if (browserOpts.networkBreadcrumbs !== false) { - const netOpts = typeof browserOpts.networkBreadcrumbs === 'object' - ? browserOpts.networkBreadcrumbs - : {}; - browserIntegrations.push( - new NetworkBreadcrumbIntegration({ ...netOpts, apiUrl }), - ); - } - - const transportWrapper = browserOpts.offlineResilience !== false - ? (inner: Transport) => new OfflineTransport({ - inner, - beaconUrl: `${apiUrl}/api/v1/ingest`, - apiKey: resolveDSN(options).apiKey, - debug: options.debug, - }) - : undefined; - - hub.init({ - ...options, - transportWrapper: transportWrapper ?? options.transportWrapper, - integrations: [ - new GlobalErrorIntegration(), - ...browserIntegrations, - ...(options.integrations ?? []), - ], - }); - - hub.getScope().setSessionId(getSessionId()); + initBrowserLogtide(options, { defaultService: 'sveltekit' }); } diff --git a/packages/types/package.json b/packages/types/package.json index 0023308..6d6dd39 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@logtide/types", - "version": "0.9.0", + "version": "0.10.0", "description": "Shared type definitions for the LogTide SDK ecosystem", "type": "module", "main": "./dist/index.cjs",