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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
80 changes: 3 additions & 77 deletions packages/angular/src/provide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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+).
*
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 8 additions & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
// Initialization
export {
initLogtide,
buildBrowserIntegrations,
buildBrowserTransportWrapper,
type InitLogtideExtraOptions,
} from './init';

// Session
export { getSessionId, resetSessionId } from './session';

Expand Down
114 changes: 114 additions & 0 deletions packages/browser/src/init.ts
Original file line number Diff line number Diff line change
@@ -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());
}
51 changes: 51 additions & 0 deletions packages/browser/tests/init.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/elysia/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/express/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/fastify/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/hono/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading
Loading