Skip to content

Commit f26f3e6

Browse files
committed
fix: use Sentry breadcrumbs for proper structured logging field extraction
1 parent ba3ee3a commit f26f3e6

File tree

8 files changed

+130
-63
lines changed

8 files changed

+130
-63
lines changed

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
## [1.11.2](https://github.com/typelets/typelets-api/compare/v1.11.1...v1.11.2) (2025-10-18)
22

3-
43
### Bug Fixes
54

6-
* standardize logging with structured data and add cache monitoring support ([534bbe9](https://github.com/typelets/typelets-api/commit/534bbe9f476d3fb92fa40897cbcc68838df66dc7))
5+
- standardize logging with structured data and add cache monitoring support ([534bbe9](https://github.com/typelets/typelets-api/commit/534bbe9f476d3fb92fa40897cbcc68838df66dc7))
76

87
## [1.11.1](https://github.com/typelets/typelets-api/compare/v1.11.0...v1.11.1) (2025-10-18)
98

src/instrument.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ if (process.env.SENTRY_DSN) {
1111

1212
integrations: [
1313
nodeProfilingIntegration(),
14-
// Send console.log, console.warn, and console.error calls as logs to Sentry
15-
Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
1614
// Automatic PostgreSQL query monitoring and performance tracking
1715
Sentry.postgresIntegration(),
16+
// Only capture unexpected console.error (logger uses breadcrumbs for structured logging)
17+
// This catches errors from third-party libraries or unexpected crashes
18+
Sentry.captureConsoleIntegration({ levels: ["error"] }),
1819
],
1920

2021
// Send structured logs to Sentry

src/lib/logger.ts

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as Sentry from "@sentry/node";
2+
13
interface LogLevel {
24
level: string;
35
priority: number;
@@ -33,48 +35,69 @@ class Logger {
3335
return level.priority <= this.currentLogLevel.priority;
3436
}
3537

36-
private formatLog(
37-
level: string,
38+
private sendToSentry(
39+
level: "error" | "warning" | "info" | "debug",
3840
message: string,
3941
meta: LogMetadata = {}
40-
): Record<string, unknown> {
41-
return {
42-
timestamp: new Date().toISOString(),
42+
): void {
43+
// Send as breadcrumb for proper field extraction in Sentry
44+
Sentry.addBreadcrumb({
4345
level,
44-
service: this.service,
45-
environment: this.environment,
46-
version: this.version,
4746
message,
48-
...meta,
49-
};
47+
category: (meta.type as string) || "app",
48+
data: {
49+
service: this.service,
50+
environment: this.environment,
51+
version: this.version,
52+
...meta,
53+
},
54+
});
5055
}
5156

5257
error(message: string, meta: LogMetadata = {}, error?: Error): void {
5358
if (this.shouldLog(LOG_LEVELS.error)) {
54-
const logData = this.formatLog("error", message, meta);
59+
const enrichedMeta = { ...meta };
5560
if (error) {
56-
logData.errorMessage = error.message;
57-
logData.errorStack = error.stack;
61+
enrichedMeta.errorMessage = error.message;
62+
enrichedMeta.errorStack = error.stack || "";
63+
}
64+
65+
this.sendToSentry("error", message, enrichedMeta);
66+
67+
// Also send to console for CloudWatch
68+
if (this.environment === "development") {
69+
console.error(message, enrichedMeta);
5870
}
59-
console.error(message, logData);
6071
}
6172
}
6273

6374
warn(message: string, meta: LogMetadata = {}): void {
6475
if (this.shouldLog(LOG_LEVELS.warn)) {
65-
console.warn(message, this.formatLog("warn", message, meta));
76+
this.sendToSentry("warning", message, meta);
77+
78+
if (this.environment === "development") {
79+
console.warn(message, meta);
80+
}
6681
}
6782
}
6883

6984
info(message: string, meta: LogMetadata = {}): void {
7085
if (this.shouldLog(LOG_LEVELS.info)) {
71-
console.log(message, this.formatLog("info", message, meta));
86+
this.sendToSentry("info", message, meta);
87+
88+
if (this.environment === "development") {
89+
console.log(message, meta);
90+
}
7291
}
7392
}
7493

7594
debug(message: string, meta: LogMetadata = {}): void {
7695
if (this.shouldLog(LOG_LEVELS.debug)) {
77-
console.log(message, this.formatLog("debug", message, meta));
96+
this.sendToSentry("debug", message, meta);
97+
98+
if (this.environment === "development") {
99+
console.log(message, meta);
100+
}
78101
}
79102
}
80103

src/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
// This file is automatically updated by semantic-release
2-
export const VERSION = "1.11.2"
2+
export const VERSION = "1.11.2";

src/websocket/auth/handler.ts

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { verifyToken } from "@clerk/backend";
22
import { createHash, createHmac } from "crypto";
33
import { AuthenticatedWebSocket, WebSocketMessage, WebSocketConfig } from "../types";
44
import { ConnectionManager } from "../middleware/connection-manager";
5+
import { logger } from "../../lib/logger";
56

67
interface AuthenticatedMessage {
78
payload: WebSocketMessage;
@@ -41,7 +42,10 @@ export class AuthHandler {
4142

4243
// Emergency cleanup if too many nonces (DoS protection)
4344
if (this.usedNonces.size > this.MAX_NONCES) {
44-
console.warn(`Nonce storage exceeded limit (${this.MAX_NONCES}), clearing all nonces`);
45+
logger.warn("Nonce storage exceeded limit, clearing all nonces", {
46+
type: "websocket_security",
47+
maxNonces: this.MAX_NONCES,
48+
});
4549
this.usedNonces.clear();
4650
}
4751
}
@@ -124,8 +128,11 @@ export class AuthHandler {
124128
console.log(`User ${ws.userId} authenticated via WebSocket`);
125129
}
126130
} catch (error: unknown) {
127-
const errorMessage = error instanceof Error ? error.message : String(error);
128-
console.error("WebSocket authentication failed", { error: errorMessage });
131+
logger.error(
132+
"WebSocket authentication failed",
133+
{ type: "websocket_auth_error" },
134+
error instanceof Error ? error : new Error(String(error))
135+
);
129136

130137
const isTokenExpired = (error as Record<string, unknown>)?.reason === "token-expired";
131138

@@ -161,21 +168,29 @@ export class AuthHandler {
161168
const MAX_MESSAGE_AGE = 5 * 60 * 1000; // 5 minutes
162169
if (messageAge > MAX_MESSAGE_AGE || messageAge < -60000) {
163170
// -60 seconds tolerance for clock skew
164-
console.warn("Message rejected: timestamp out of range");
171+
logger.warn("Message rejected: timestamp out of range", {
172+
type: "websocket_security",
173+
messageAge,
174+
maxAge: MAX_MESSAGE_AGE,
175+
});
165176
return false;
166177
}
167178

168179
// 2. Check for replay attack using nonce
169180
const nonceKey = `${nonce}:${timestamp}`;
170181
if (this.usedNonces.has(nonceKey)) {
171-
console.warn("Message rejected: nonce already used (replay attack)");
182+
logger.warn("Message rejected: nonce already used (replay attack)", {
183+
type: "websocket_security",
184+
});
172185
return false;
173186
}
174187

175188
try {
176189
// 3. Validate required parameters
177190
if (!jwtToken || !userId) {
178-
console.error("Missing JWT token or user ID for signature verification");
191+
logger.warn("Missing JWT token or user ID for signature verification", {
192+
type: "websocket_security",
193+
});
179194
return false;
180195
}
181196

@@ -207,9 +222,10 @@ export class AuthHandler {
207222
const isValid = isValidRegenerated || isValidStored;
208223

209224
if (!isValid) {
210-
console.warn("Message signature verification failed for user", userId);
211-
} else {
212-
console.debug("Message signature verified successfully for user", userId);
225+
logger.warn("Message signature verification failed", {
226+
type: "websocket_security",
227+
userId: userId || "unknown",
228+
});
213229
}
214230

215231
if (isValid) {
@@ -219,8 +235,11 @@ export class AuthHandler {
219235

220236
return isValid;
221237
} catch (error) {
222-
const errorMessage = error instanceof Error ? error.message : String(error);
223-
console.error("Error verifying message signature", { error: errorMessage });
238+
logger.error(
239+
"Error verifying message signature",
240+
{ type: "websocket_auth_error" },
241+
error instanceof Error ? error : new Error(String(error))
242+
);
224243
return false;
225244
}
226245
}
@@ -239,7 +258,9 @@ export class AuthHandler {
239258
if (this.isAuthenticatedMessage(rawMessage)) {
240259
// This is an authenticated message, verify signature
241260
if (!ws.sessionSecret) {
242-
console.warn("Authenticated message received but no session secret available");
261+
logger.warn("Authenticated message received but no session secret available", {
262+
type: "websocket_security",
263+
});
243264
return null;
244265
}
245266

@@ -250,7 +271,10 @@ export class AuthHandler {
250271
ws.userId
251272
);
252273
if (!isValid) {
253-
console.warn("Message signature verification failed for user", ws.userId);
274+
logger.warn("Message signature verification failed", {
275+
type: "websocket_security",
276+
userId: ws.userId || "unknown",
277+
});
254278
return null;
255279
}
256280

src/websocket/handlers/base.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,15 @@ export class BaseResourceHandler {
6767
return;
6868
}
6969
} catch (error) {
70-
const errorMessage = error instanceof Error ? error.message : String(error);
71-
console.error(`Error authorizing ${config.resourceType} ${config.operation}`, {
72-
resourceType: config.resourceType,
73-
operation: config.operation,
74-
error: errorMessage,
75-
});
70+
logger.error(
71+
`Error authorizing ${config.resourceType} ${config.operation}`,
72+
{
73+
type: "websocket_error",
74+
resourceType: config.resourceType,
75+
operation: config.operation,
76+
},
77+
error instanceof Error ? error : new Error(String(error))
78+
);
7679
ws.send(
7780
JSON.stringify({
7881
type: "error",

src/websocket/handlers/notes.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ export class NoteHandler extends BaseResourceHandler {
5454
})
5555
);
5656
} catch (error) {
57-
const errorMessage = error instanceof Error ? error.message : String(error);
58-
console.error("Error joining note", { noteId: message.noteId, error: errorMessage });
57+
logger.error(
58+
"Error joining note",
59+
{ type: "websocket_error", noteId: message.noteId || "unknown" },
60+
error instanceof Error ? error : new Error(String(error))
61+
);
5962
ws.send(
6063
JSON.stringify({
6164
type: "error",
@@ -137,17 +140,21 @@ export class NoteHandler extends BaseResourceHandler {
137140
typeof value === "string" &&
138141
value !== "[ENCRYPTED]"
139142
) {
140-
console.warn(
141-
`Note update: rejected plaintext ${key} for note ${message.noteId} - must be [ENCRYPTED]`
142-
);
143+
logger.warn("Note update: rejected plaintext field - must be [ENCRYPTED]", {
144+
type: "security",
145+
field: key,
146+
noteId: message.noteId || "unknown",
147+
});
143148
return;
144149
}
145150

146151
filteredChanges[key] = value;
147152
} else {
148-
console.warn(
149-
`Note update: filtered out disallowed field '${key}' for note ${message.noteId}`
150-
);
153+
logger.warn("Note update: filtered out disallowed field", {
154+
type: "security",
155+
field: key,
156+
noteId: message.noteId || "unknown",
157+
});
151158
}
152159
});
153160

@@ -209,9 +216,11 @@ export class NoteHandler extends BaseResourceHandler {
209216
})
210217
);
211218
} else {
212-
console.warn(
213-
`Note update: no valid changes found for note ${message.noteId}, attempted fields: ${Object.keys(message.changes || {}).join(", ")}`
214-
);
219+
logger.warn("Note update: no valid changes found", {
220+
type: "validation",
221+
noteId: message.noteId || "unknown",
222+
attemptedFields: Object.keys(message.changes || {}).join(", "),
223+
});
215224
ws.send(
216225
JSON.stringify({
217226
type: "error",
@@ -221,8 +230,11 @@ export class NoteHandler extends BaseResourceHandler {
221230
}
222231
}
223232
} catch (error) {
224-
const errorMessage = error instanceof Error ? error.message : String(error);
225-
console.error("Error handling note update", { noteId: message.noteId, error: errorMessage });
233+
logger.error(
234+
"Error handling note update",
235+
{ type: "websocket_error", noteId: message.noteId || "unknown" },
236+
error instanceof Error ? error : new Error(String(error))
237+
);
226238
ws.send(
227239
JSON.stringify({
228240
type: "error",

src/websocket/index.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,11 @@ export class WebSocketManager {
7878
message: "Message too large. Maximum size is 1MB.",
7979
})
8080
);
81-
console.warn(
82-
`WebSocket message too large: ${data.length} bytes from user ${ws.userId || "unauthenticated"}`
83-
);
81+
logger.warn("WebSocket message too large", {
82+
type: "websocket_security",
83+
messageSize: data.length,
84+
userId: ws.userId || "unauthenticated",
85+
});
8486
return;
8587
}
8688

@@ -129,11 +131,14 @@ export class WebSocketManager {
129131
);
130132
}
131133
} catch (error) {
132-
const errorMessage = error instanceof Error ? error.message : String(error);
133-
console.error("Error handling WebSocket message", {
134-
messageType: rawMessage?.type || "unknown",
135-
error: errorMessage,
136-
});
134+
logger.error(
135+
"Error handling WebSocket message",
136+
{
137+
type: "websocket_error",
138+
messageType: rawMessage?.type || "unknown",
139+
},
140+
error instanceof Error ? error : new Error(String(error))
141+
);
137142

138143
// WebSocket metrics WebSocket message errors
139144
// Error tracking available via console logs
@@ -166,7 +171,7 @@ export class WebSocketManager {
166171
});
167172

168173
ws.on("error", (error: Error): void => {
169-
console.error("WebSocket error", { error: error.message });
174+
logger.error("WebSocket error", { type: "websocket_connection_error" }, error);
170175

171176
// Send WebSocket connection errors to New Relic
172177
// WebSocket metrics WebSocket connection errors

0 commit comments

Comments
 (0)