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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ 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.6.0] - 2026-02-28

### Added
- **OTLP Span Events**: Breadcrumbs are now automatically converted to OTLP Span Events, providing a detailed timeline of events within the trace viewer.
- **Child Spans API**: New `startChildSpan()` and `finishChildSpan()` APIs in `@logtide/core` to create hierarchical spans for operations like DB queries or external API calls.
- **Rich Span Attributes**: Added standardized attributes to request spans across all frameworks:
- `http.user_agent`, `net.peer.ip`, `http.query_string` (at start)
- `http.status_code`, `duration_ms`, `http.route` (at finish)
- **Express Error Handler**: Exported `logtideErrorHandler` to capture unhandled errors and associate them with the current request scope.

### Changed
- **Enriched Breadcrumbs**: Request/Response breadcrumbs now include more metadata (`method`, `url`, `status`, `duration_ms`) by default.
- **Improved Nuxt Tracing**: Nitro plugin now accurately captures response status codes and durations.
- **Improved Angular Tracing**: `LogtideHttpInterceptor` now captures status codes for both successful and failed outgoing requests.

### Fixed
- Fixed a bug in Nuxt Nitro plugin where spans were always marked as 'ok' regardless of the actual response status.

## [0.5.6] - 2026-02-08

### Changed
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.5.6",
"version": "0.6.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.5.6",
"version": "0.6.0",
"description": "LogTide SDK integration for Angular — ErrorHandler, HTTP Interceptor, trace propagation",
"type": "module",
"main": "./dist/index.cjs",
Expand Down
49 changes: 41 additions & 8 deletions packages/angular/src/http-interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
HttpHandler,
HttpEvent,
HttpErrorResponse,
HttpResponse,
} from '@angular/common/http';
import { Observable, tap } from 'rxjs';
import { hub, createTraceparent, generateSpanId } from '@logtide/core';
import { hub, createTraceparent } from '@logtide/core';

/**
* Angular HTTP Interceptor that:
Expand All @@ -26,6 +27,7 @@ export class LogtideHttpInterceptor implements HttpInterceptor {

// Start a span for this outgoing request
let spanId: string | undefined;
const startTime = Date.now();

if (client) {
const span = client.startSpan({
Expand All @@ -35,6 +37,7 @@ export class LogtideHttpInterceptor implements HttpInterceptor {
attributes: {
'http.method': req.method,
'http.url': req.urlWithParams,
'http.target': req.url,
},
});

Expand All @@ -52,22 +55,50 @@ export class LogtideHttpInterceptor implements HttpInterceptor {
type: 'http',
category: 'http.request',
message: `${req.method} ${req.urlWithParams}`,
timestamp: Date.now(),
timestamp: startTime,
data: { method: req.method, url: req.urlWithParams },
});
}

return next.handle(clonedReq).pipe(
tap({
next: () => {
// On success, finish span
if (client && spanId) {
client.finishSpan(spanId, 'ok');
next: (event: HttpEvent<unknown>) => {
if (event instanceof HttpResponse) {
// On success, finish span with status code
if (client && spanId) {
const durationMs = Date.now() - startTime;
client.finishSpan(spanId, event.status >= 500 ? 'error' : 'ok', {
extraAttributes: {
'http.status_code': event.status,
'duration_ms': durationMs,
},
});

hub.addBreadcrumb({
type: 'http',
category: 'http.response',
message: `${req.method} ${req.urlWithParams} → ${event.status}`,
level: event.status >= 400 ? 'warn' : 'info',
timestamp: Date.now(),
data: {
method: req.method,
url: req.urlWithParams,
status: event.status,
duration_ms: durationMs,
},
});
}
}
},
error: (error: HttpErrorResponse) => {
const durationMs = Date.now() - startTime;
if (client && spanId) {
client.finishSpan(spanId, 'error');
client.finishSpan(spanId, 'error', {
extraAttributes: {
'http.status_code': error.status,
'duration_ms': durationMs,
},
});
}

hub.addBreadcrumb({
Expand All @@ -81,13 +112,15 @@ export class LogtideHttpInterceptor implements HttpInterceptor {
url: req.urlWithParams,
status: error.status,
statusText: error.statusText,
duration_ms: durationMs,
},
});

hub.captureError(error, {
'http.method': req.method,
'http.url': req.urlWithParams,
'http.status': error.status,
'http.status_code': error.status,
'duration_ms': durationMs,
});
},
}),
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.5.6",
"version": "0.6.0",
"description": "Core client, hub, scope, transports, and utilities for the LogTide SDK",
"type": "module",
"main": "./dist/index.cjs",
Expand Down
33 changes: 33 additions & 0 deletions packages/core/src/child-span.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Span, SpanAttributes, SpanEvent } from '@logtide/types';
import { hub } from './hub';
import type { Scope } from './scope';

/**
* Start a child span under the given scope.
* If no client is registered, returns a no-op span.
*/
export function startChildSpan(name: string, scope: Scope, attributes?: SpanAttributes): Span {
const client = hub.getClient();
if (!client) {
return {
traceId: scope.traceId,
spanId: '0000000000000000',
name,
status: 'unset',
startTime: Date.now(),
attributes: attributes ?? {},
};
}
return client.startChildSpan(name, scope, attributes);
}

/**
* Finish a child span by ID via the hub client.
*/
export function finishChildSpan(
spanId: string,
status: 'ok' | 'error' = 'ok',
options?: { extraAttributes?: SpanAttributes; events?: SpanEvent[] },
): void {
hub.getClient()?.finishChildSpan(spanId, status, options);
}
38 changes: 35 additions & 3 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
Integration,
LogLevel,
Span,
SpanAttributes,
SpanEvent,
Transport,
} from '@logtide/types';
import { resolveDSN } from './dsn';
Expand Down Expand Up @@ -41,7 +43,10 @@ class DefaultTransport implements Transport {
});

this.spanTransport = new BatchTransport({
inner: new OtlpHttpTransport(dsn, options.service || 'unknown'),
inner: new OtlpHttpTransport(dsn, options.service || 'unknown', {
environment: options.environment,
release: options.release,
}),
batchSize: options.batchSize,
flushInterval: options.flushInterval,
maxBufferSize: options.maxBufferSize,
Expand Down Expand Up @@ -189,13 +194,40 @@ export class LogtideClient implements IClient {
return this.spanManager.startSpan(options);
}

finishSpan(spanId: string, status: 'ok' | 'error' = 'ok'): void {
const span = this.spanManager.finishSpan(spanId, status);
finishSpan(
spanId: string,
status: 'ok' | 'error' = 'ok',
options?: { extraAttributes?: SpanAttributes; events?: SpanEvent[] },
): void {
const span = this.spanManager.finishSpan(spanId, status, options);
if (span && this.transport.sendSpans) {
this.transport.sendSpans([span]);
}
}

/**
* Start a child span under the given scope.
*/
startChildSpan(name: string, scope: Scope, attributes?: SpanAttributes): Span {
return this.startSpan({
name,
traceId: scope.traceId,
parentSpanId: scope.spanId,
attributes,
});
}

/**
* Finish a child span by ID.
*/
finishChildSpan(
spanId: string,
status: 'ok' | 'error' = 'ok',
options?: { extraAttributes?: SpanAttributes; events?: SpanEvent[] },
): void {
this.finishSpan(spanId, status, options);
}

// ─── Integrations ─────────────────────────────────────

addIntegration(integration: Integration): void {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type {
Span,
SpanStatus,
SpanAttributes,
SpanEvent,
Breadcrumb,
BreadcrumbType,
Transport,
Expand All @@ -21,6 +22,7 @@ export { hub } from './hub';
export { Scope } from './scope';
export { SpanManager, type StartSpanOptions } from './span-manager';
export { BreadcrumbBuffer } from './breadcrumb-buffer';
export { startChildSpan, finishChildSpan } from './child-span';

// DSN
export { parseDSN, resolveDSN } from './dsn';
Expand Down
18 changes: 16 additions & 2 deletions packages/core/src/span-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Span, SpanAttributes, SpanStatus } from '@logtide/types';
import type { Span, SpanAttributes, SpanEvent, SpanStatus } from '@logtide/types';
import { generateSpanId, generateTraceId } from './utils/trace-id';

export interface StartSpanOptions {
Expand Down Expand Up @@ -26,12 +26,26 @@ export class SpanManager {
return span;
}

finishSpan(spanId: string, status: SpanStatus = 'ok'): Span | undefined {
finishSpan(
spanId: string,
status: SpanStatus = 'ok',
options?: { extraAttributes?: SpanAttributes; events?: SpanEvent[] },
): Span | undefined {
const span = this.activeSpans.get(spanId);
if (!span) return undefined;

span.endTime = Date.now();
span.status = status;

if (options) {
if (options.extraAttributes) {
Object.assign(span.attributes, options.extraAttributes);
}
if (options.events && options.events.length > 0) {
span.events = (span.events ?? []).concat(options.events);
}
}

this.activeSpans.delete(spanId);
return span;
}
Expand Down
Loading