Skip to content

Commit 6a8ae04

Browse files
authored
Merge pull request #7 from logtide-dev/feature/browser-sdk
better browser sdk implementation
2 parents c1d0884 + 63d6cf0 commit 6a8ae04

83 files changed

Lines changed: 5963 additions & 1036 deletions

File tree

Some content is hidden

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

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.6.1] - 2026-02-28
9+
10+
### Fixed
11+
- **Security Updates**: Addressed multiple security vulnerabilities across the workspace:
12+
- Updated `minimatch` to `>=10.2.3` (fixes several ReDoS vulnerabilities).
13+
- Updated `rollup` to `>=4.59.0` (fixes Arbitrary File Write via Path Traversal).
14+
- Updated `tar` to `>=7.5.8` (fixes Hardlink Target Escape).
15+
- Updated `nanotar` to `^0.2.1` (fixes Path Traversal).
16+
- Updated `@angular/core` to `^19.2.19` (fixes XSS in i18n).
17+
- Updated `@sveltejs/kit` to `^2.52.2` and `svelte` to `^5.53.5` (fixes XSS and Resource Exhaustion).
18+
- Updated `ajv` to `>=8.18.0` (fixes ReDoS).
19+
- Updated `qs` to `>=6.14.2` (fixes DoS).
20+
- Updated `hono` to `^4.11.10` (Timing attack hardening).
21+
- Updated `devalue` to `>=5.6.3` (fixes Prototype Pollution and Resource Exhaustion).
22+
23+
## [0.6.0] - 2026-02-28
24+
25+
### Added
26+
- **OTLP Span Events**: Breadcrumbs are now automatically converted to OTLP Span Events, providing a detailed timeline of events within the trace viewer.
27+
- **Child Spans API**: New `startChildSpan()` and `finishChildSpan()` APIs in `@logtide/core` to create hierarchical spans for operations like DB queries or external API calls.
28+
- **Rich Span Attributes**: Added standardized attributes to request spans across all frameworks:
29+
- `http.user_agent`, `net.peer.ip`, `http.query_string` (at start)
30+
- `http.status_code`, `duration_ms`, `http.route` (at finish)
31+
- **Express Error Handler**: Exported `logtideErrorHandler` to capture unhandled errors and associate them with the current request scope.
32+
33+
### Changed
34+
- **Enriched Breadcrumbs**: Request/Response breadcrumbs now include more metadata (`method`, `url`, `status`, `duration_ms`) by default.
35+
- **Improved Nuxt Tracing**: Nitro plugin now accurately captures response status codes and durations.
36+
- **Improved Angular Tracing**: `LogtideHttpInterceptor` now captures status codes for both successful and failed outgoing requests.
37+
38+
### Fixed
39+
- Fixed a bug in Nuxt Nitro plugin where spans were always marked as 'ok' regardless of the actual response status.
40+
841
## [0.5.6] - 2026-02-08
942

1043
### Changed

package.json

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"private": true,
3-
"version": "0.5.6",
3+
"version": "0.6.1",
44
"scripts": {
55
"build": "pnpm -r --filter @logtide/* build",
66
"test": "pnpm -r --filter @logtide/* test",
@@ -16,10 +16,20 @@
1616
},
1717
"pnpm": {
1818
"overrides": {
19-
"tar": ">=7.5.7",
19+
"tar": ">=7.5.8",
2020
"esbuild": ">=0.25.0",
2121
"webpack": ">=5.104.1",
22-
"cookie": ">=0.7.0"
22+
"cookie": ">=0.7.0",
23+
"minimatch": ">=10.2.3",
24+
"rollup": ">=4.59.0",
25+
"ajv": ">=8.18.0",
26+
"qs": ">=6.14.2",
27+
"devalue": ">=5.6.3",
28+
"hono": "^4.11.10",
29+
"@angular/core": "^19.2.19",
30+
"@sveltejs/kit": "^2.52.2",
31+
"svelte": "^5.53.5",
32+
"nanotar": "^0.2.1"
2333
}
2434
}
2535
}

packages/angular/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@logtide/angular",
3-
"version": "0.5.6",
3+
"version": "0.6.1",
44
"description": "LogTide SDK integration for Angular — ErrorHandler, HTTP Interceptor, trace propagation",
55
"type": "module",
66
"main": "./dist/index.cjs",
@@ -42,6 +42,7 @@
4242
"access": "public"
4343
},
4444
"dependencies": {
45+
"@logtide/browser": "workspace:*",
4546
"@logtide/core": "workspace:*",
4647
"@logtide/types": "workspace:*"
4748
},
@@ -51,9 +52,9 @@
5152
"rxjs": ">=7.0.0"
5253
},
5354
"devDependencies": {
54-
"@angular/common": "^19.0.0",
55-
"@angular/compiler": "^21.1.3",
56-
"@angular/core": "^19.0.0",
55+
"@angular/common": "^19.2.19",
56+
"@angular/compiler": "^19.2.19",
57+
"@angular/core": "^19.2.19",
5758
"rxjs": "^7.8.0",
5859
"tsup": "^8.5.1",
5960
"typescript": "^5.5.4"

packages/angular/src/error-handler.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1-
import { ErrorHandler, Injectable } from '@angular/core';
1+
import { ErrorHandler, Injectable, NgZone } from '@angular/core';
22
import { hub } from '@logtide/core';
33

44
/**
55
* Angular ErrorHandler that reports uncaught errors to LogTide.
66
*
7+
* Detects whether the error occurred inside or outside NgZone and tags
8+
* the error accordingly. Errors outside NgZone often indicate issues with
9+
* third-party libraries or manual DOM manipulation.
10+
*
711
* Used automatically when you call `provideLogtide()` or import `LogtideModule`.
812
*/
913
@Injectable()
1014
export class LogtideErrorHandler implements ErrorHandler {
1115
handleError(error: unknown): void {
16+
const zoneContext = NgZone.isInAngularZone() ? 'inside' : 'outside';
17+
1218
hub.captureError(error, {
1319
mechanism: 'angular.errorHandler',
20+
'angular.zone': zoneContext,
1421
});
1522

1623
// Also log to console so errors remain visible in dev

packages/angular/src/http-interceptor.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import {
55
HttpHandler,
66
HttpEvent,
77
HttpErrorResponse,
8+
HttpResponse,
89
} from '@angular/common/http';
910
import { Observable, tap } from 'rxjs';
10-
import { hub, createTraceparent, generateSpanId } from '@logtide/core';
11+
import { hub, createTraceparent } from '@logtide/core';
1112

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

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

3032
if (client) {
3133
const span = client.startSpan({
@@ -35,6 +37,7 @@ export class LogtideHttpInterceptor implements HttpInterceptor {
3537
attributes: {
3638
'http.method': req.method,
3739
'http.url': req.urlWithParams,
40+
'http.target': req.url,
3841
},
3942
});
4043

@@ -52,22 +55,50 @@ export class LogtideHttpInterceptor implements HttpInterceptor {
5255
type: 'http',
5356
category: 'http.request',
5457
message: `${req.method} ${req.urlWithParams}`,
55-
timestamp: Date.now(),
58+
timestamp: startTime,
5659
data: { method: req.method, url: req.urlWithParams },
5760
});
5861
}
5962

6063
return next.handle(clonedReq).pipe(
6164
tap({
62-
next: () => {
63-
// On success, finish span
64-
if (client && spanId) {
65-
client.finishSpan(spanId, 'ok');
65+
next: (event: HttpEvent<unknown>) => {
66+
if (event instanceof HttpResponse) {
67+
// On success, finish span with status code
68+
if (client && spanId) {
69+
const durationMs = Date.now() - startTime;
70+
client.finishSpan(spanId, event.status >= 500 ? 'error' : 'ok', {
71+
extraAttributes: {
72+
'http.status_code': event.status,
73+
'duration_ms': durationMs,
74+
},
75+
});
76+
77+
hub.addBreadcrumb({
78+
type: 'http',
79+
category: 'http.response',
80+
message: `${req.method} ${req.urlWithParams}${event.status}`,
81+
level: event.status >= 400 ? 'warn' : 'info',
82+
timestamp: Date.now(),
83+
data: {
84+
method: req.method,
85+
url: req.urlWithParams,
86+
status: event.status,
87+
duration_ms: durationMs,
88+
},
89+
});
90+
}
6691
}
6792
},
6893
error: (error: HttpErrorResponse) => {
94+
const durationMs = Date.now() - startTime;
6995
if (client && spanId) {
70-
client.finishSpan(spanId, 'error');
96+
client.finishSpan(spanId, 'error', {
97+
extraAttributes: {
98+
'http.status_code': error.status,
99+
'duration_ms': durationMs,
100+
},
101+
});
71102
}
72103

73104
hub.addBreadcrumb({
@@ -81,13 +112,15 @@ export class LogtideHttpInterceptor implements HttpInterceptor {
81112
url: req.urlWithParams,
82113
status: error.status,
83114
statusText: error.statusText,
115+
duration_ms: durationMs,
84116
},
85117
});
86118

87119
hub.captureError(error, {
88120
'http.method': req.method,
89121
'http.url': req.urlWithParams,
90-
'http.status': error.status,
122+
'http.status_code': error.status,
123+
'duration_ms': durationMs,
91124
});
92125
},
93126
}),

packages/angular/src/provide.ts

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,64 @@ import {
66
APP_INITIALIZER,
77
} from '@angular/core';
88
import { HTTP_INTERCEPTORS } from '@angular/common/http';
9-
import type { ClientOptions } from '@logtide/types';
10-
import { hub, GlobalErrorIntegration } from '@logtide/core';
9+
import type { Integration, Transport } from '@logtide/types';
10+
import { hub, GlobalErrorIntegration, resolveDSN } from '@logtide/core';
11+
import {
12+
getSessionId,
13+
WebVitalsIntegration,
14+
ClickBreadcrumbIntegration,
15+
NetworkBreadcrumbIntegration,
16+
OfflineTransport,
17+
type BrowserClientOptions,
18+
} from '@logtide/browser';
1119
import { LogtideErrorHandler } from './error-handler';
1220
import { LogtideHttpInterceptor } from './http-interceptor';
1321

22+
function buildBrowserIntegrations(options: BrowserClientOptions): Integration[] {
23+
const browserOpts = options.browser ?? {};
24+
const integrations: Integration[] = [];
25+
const apiUrl = resolveDSN(options).apiUrl;
26+
27+
if (browserOpts.webVitals) {
28+
integrations.push(
29+
new WebVitalsIntegration({
30+
sampleRate: browserOpts.webVitalsSampleRate,
31+
}),
32+
);
33+
}
34+
35+
if (browserOpts.clickBreadcrumbs !== false) {
36+
const clickOpts = typeof browserOpts.clickBreadcrumbs === 'object'
37+
? browserOpts.clickBreadcrumbs
38+
: undefined;
39+
integrations.push(new ClickBreadcrumbIntegration(clickOpts));
40+
}
41+
42+
if (browserOpts.networkBreadcrumbs !== false) {
43+
const netOpts = typeof browserOpts.networkBreadcrumbs === 'object'
44+
? browserOpts.networkBreadcrumbs
45+
: {};
46+
integrations.push(
47+
new NetworkBreadcrumbIntegration({ ...netOpts, apiUrl }),
48+
);
49+
}
50+
51+
return integrations;
52+
}
53+
54+
function buildTransportWrapper(options: BrowserClientOptions): ((inner: Transport) => Transport) | undefined {
55+
const browserOpts = options.browser ?? {};
56+
if (browserOpts.offlineResilience === false) return undefined;
57+
58+
const dsn = resolveDSN(options);
59+
return (inner: Transport) => new OfflineTransport({
60+
inner,
61+
beaconUrl: `${dsn.apiUrl}/api/v1/ingest`,
62+
apiKey: dsn.apiKey,
63+
debug: options.debug,
64+
});
65+
}
66+
1467
/**
1568
* Provide LogTide in a standalone Angular app (Angular 17+).
1669
*
@@ -26,7 +79,7 @@ import { LogtideHttpInterceptor } from './http-interceptor';
2679
* };
2780
* ```
2881
*/
29-
export function provideLogtide(options: ClientOptions): EnvironmentProviders {
82+
export function provideLogtide(options: BrowserClientOptions): EnvironmentProviders {
3083
return makeEnvironmentProviders([
3184
{
3285
provide: APP_INITIALIZER,
@@ -35,11 +88,14 @@ export function provideLogtide(options: ClientOptions): EnvironmentProviders {
3588
hub.init({
3689
service: 'angular',
3790
...options,
91+
transportWrapper: buildTransportWrapper(options) ?? options.transportWrapper,
3892
integrations: [
3993
new GlobalErrorIntegration(),
94+
...buildBrowserIntegrations(options),
4095
...(options.integrations ?? []),
4196
],
4297
});
98+
hub.getScope().setSessionId(getSessionId());
4399
};
44100
},
45101
multi: true,
@@ -66,7 +122,7 @@ export function provideLogtide(options: ClientOptions): EnvironmentProviders {
66122
* export class AppModule {}
67123
* ```
68124
*/
69-
export function getLogtideProviders(options: ClientOptions): Provider[] {
125+
export function getLogtideProviders(options: BrowserClientOptions): Provider[] {
70126
return [
71127
{
72128
provide: APP_INITIALIZER,
@@ -75,11 +131,14 @@ export function getLogtideProviders(options: ClientOptions): Provider[] {
75131
hub.init({
76132
service: 'angular',
77133
...options,
134+
transportWrapper: buildTransportWrapper(options) ?? options.transportWrapper,
78135
integrations: [
79136
new GlobalErrorIntegration(),
137+
...buildBrowserIntegrations(options),
80138
...(options.integrations ?? []),
81139
],
82140
});
141+
hub.getScope().setSessionId(getSessionId());
83142
};
84143
},
85144
multi: true,

packages/angular/tests/error-handler.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ describe('@logtide/angular', () => {
6969
expect(transport.logs).toHaveLength(1);
7070
expect(transport.logs[0].message).toBe('string error');
7171
});
72+
73+
it('should include angular.zone context in metadata', async () => {
74+
const { LogtideErrorHandler } = await import('../src/error-handler');
75+
const handler = new LogtideErrorHandler();
76+
77+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
78+
handler.handleError(new Error('zone error'));
79+
consoleSpy.mockRestore();
80+
81+
expect(transport.logs).toHaveLength(1);
82+
// Outside of Angular context, NgZone.isInAngularZone() returns false
83+
expect(transport.logs[0].metadata?.['angular.zone']).toBe('outside');
84+
});
7285
});
7386

7487
describe('provideLogtide', () => {

0 commit comments

Comments
 (0)