Skip to content

Commit 422a23c

Browse files
committed
fix: send logs as Sentry events using captureMessage/captureException
1 parent 230694e commit 422a23c

File tree

9 files changed

+140
-357
lines changed

9 files changed

+140
-357
lines changed

.github/workflows/sentry-release.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ jobs:
1515
fetch-depth: 0 # Fetch all history for commit tracking
1616

1717
- name: Set Release Version from Tag
18-
run: echo "RELEASE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV
18+
run: |
19+
# Strip 'v' prefix from tag to match npm version format
20+
VERSION=${{ github.ref_name }}
21+
echo "RELEASE_VERSION=${VERSION#v}" >> $GITHUB_ENV
1922
2023
- name: Install Sentry CLI
2124
run: curl -sL https://sentry.io/get-cli/ | bash

CHANGELOG.md

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

3-
43
### Bug Fixes
54

6-
* send logs as Sentry events using captureMessage/captureException ([3133f21](https://github.com/typelets/typelets-api/commit/3133f21de3319f642d4cfbde889e978d6ed4745a))
5+
- send logs as Sentry events using captureMessage/captureException ([3133f21](https://github.com/typelets/typelets-api/commit/3133f21de3319f642d4cfbde889e978d6ed4745a))
76

87
## [1.11.3](https://github.com/typelets/typelets-api/compare/v1.11.2...v1.11.3) (2025-10-18)
98

src/lib/logger.ts

Lines changed: 33 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,10 @@ const LOG_LEVELS: Record<string, LogLevel> = {
1616

1717
class Logger {
1818
private environment: string;
19-
private service: string;
20-
private version: string;
2119
private currentLogLevel: LogLevel;
2220

2321
constructor() {
2422
this.environment = process.env.NODE_ENV || "development";
25-
this.service = "typelets-api";
26-
this.version = process.env.npm_package_version || "1.0.0";
2723

2824
// Set log level based on environment
2925
const logLevelName =
@@ -35,102 +31,61 @@ class Logger {
3531
return level.priority <= this.currentLogLevel.priority;
3632
}
3733

38-
private sendToSentry(
39-
level: "error" | "warning" | "info" | "debug",
40-
message: string,
41-
meta: LogMetadata = {}
42-
): void {
43-
const enrichedData = {
44-
service: this.service,
45-
environment: this.environment,
46-
version: this.version,
47-
...meta,
48-
};
49-
50-
// Add breadcrumb for context (appears in transaction/error details)
51-
Sentry.addBreadcrumb({
52-
level,
53-
message,
54-
category: (meta.type as string) || "app",
55-
data: enrichedData,
56-
});
34+
private normalizePath(path: string): string {
35+
// Take first two path segments and replace UUIDs/IDs with {id}
36+
const segments = path.split("/").filter((s) => s.length > 0);
37+
const normalized = segments
38+
.slice(0, 2)
39+
.map((segment) => {
40+
// Replace UUIDs, numeric IDs, or other dynamic identifiers with {id}
41+
if (
42+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment) ||
43+
/^\d+$/.test(segment) ||
44+
/^[a-zA-Z0-9_-]{20,}$/.test(segment)
45+
) {
46+
return "{id}";
47+
}
48+
return segment;
49+
})
50+
.join("/");
5751

58-
// Also send as message event so it appears in Sentry Issues
59-
// Only send info/warn/error as events (not debug to reduce noise)
60-
if (level !== "debug") {
61-
Sentry.captureMessage(message, {
62-
level,
63-
contexts: {
64-
metadata: enrichedData,
65-
},
66-
tags: {
67-
type: (meta.type as string) || "app",
68-
service: this.service,
69-
},
70-
});
71-
}
52+
return `/${normalized}`;
7253
}
7354

7455
error(message: string, meta: LogMetadata = {}, error?: Error): void {
7556
if (this.shouldLog(LOG_LEVELS.error)) {
76-
const enrichedMeta = { ...meta };
77-
7857
if (error) {
79-
// Send exception to Sentry with context
58+
// Send exception with stack trace and metadata
8059
Sentry.captureException(error, {
8160
contexts: {
82-
metadata: {
83-
service: this.service,
84-
environment: this.environment,
85-
version: this.version,
86-
message,
87-
...meta,
88-
},
61+
metadata: meta,
8962
},
9063
tags: {
9164
type: (meta.type as string) || "error",
92-
service: this.service,
9365
},
9466
});
9567
} else {
96-
// No error object, send as error message
97-
this.sendToSentry("error", message, enrichedMeta);
98-
}
99-
100-
// Also send to console for development debugging
101-
if (this.environment === "development") {
102-
console.error(message, enrichedMeta);
68+
// Send error log with metadata
69+
Sentry.logger.error(message, meta);
10370
}
10471
}
10572
}
10673

10774
warn(message: string, meta: LogMetadata = {}): void {
10875
if (this.shouldLog(LOG_LEVELS.warn)) {
109-
this.sendToSentry("warning", message, meta);
110-
111-
if (this.environment === "development") {
112-
console.warn(message, meta);
113-
}
76+
Sentry.logger.warn(message, meta);
11477
}
11578
}
11679

11780
info(message: string, meta: LogMetadata = {}): void {
11881
if (this.shouldLog(LOG_LEVELS.info)) {
119-
this.sendToSentry("info", message, meta);
120-
121-
if (this.environment === "development") {
122-
console.log(message, meta);
123-
}
82+
Sentry.logger.info(message, meta);
12483
}
12584
}
12685

12786
debug(message: string, meta: LogMetadata = {}): void {
12887
if (this.shouldLog(LOG_LEVELS.debug)) {
129-
this.sendToSentry("debug", message, meta);
130-
131-
if (this.environment === "development") {
132-
console.log(message, meta);
133-
}
88+
Sentry.logger.debug(message, meta);
13489
}
13590
}
13691

@@ -142,7 +97,8 @@ class Logger {
14297
duration: number,
14398
userId?: string
14499
): void {
145-
this.info("HTTP request completed", {
100+
const normalizedPath = this.normalizePath(path);
101+
this.info(`[API] ${method} ${normalizedPath}`, {
146102
type: "http_request",
147103
method,
148104
path,
@@ -171,11 +127,11 @@ class Logger {
171127
if (resourceType) meta.resourceType = resourceType;
172128
if (status) meta.status = status;
173129

174-
this.info("WebSocket event", meta);
130+
this.info(`WebSocket ${eventType}`, meta);
175131
}
176132

177133
databaseQuery(operation: string, table: string, duration: number, userId?: string): void {
178-
this.debug("Database query executed", {
134+
this.debug(`[DB] ${operation} ${table}`, {
179135
type: "database_query",
180136
operation,
181137
table,
@@ -185,7 +141,8 @@ class Logger {
185141
}
186142

187143
codeExecution(languageId: number, duration: number, success: boolean, userId?: string): void {
188-
this.info("Code execution completed", {
144+
const status = success ? "success" : "failed";
145+
this.info(`Code execution ${status}`, {
189146
type: "code_execution",
190147
languageId,
191148
duration,
@@ -195,7 +152,7 @@ class Logger {
195152
}
196153

197154
businessEvent(eventName: string, userId: string, metadata: LogMetadata = {}): void {
198-
this.info("Business event", {
155+
this.info(`Business event ${eventName}`, {
199156
type: "business_event",
200157
eventName,
201158
userId,
@@ -208,7 +165,7 @@ class Logger {
208165
severity: "low" | "medium" | "high" | "critical",
209166
details: LogMetadata
210167
): void {
211-
this.warn("Security event detected", {
168+
this.warn(`Security event ${eventType}`, {
212169
type: "security_event",
213170
eventType,
214171
severity,

src/middleware/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ export const authMiddleware = async (c: Context, next: Next) => {
152152
}
153153
} else {
154154
logger.error(
155-
"Database error creating user",
155+
"[DB] Error creating user",
156156
{
157157
userId: userData.id,
158158
error: error instanceof Error ? error.message : String(error),

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.4"
2+
export const VERSION = "1.11.4";

src/websocket/auth/handler.ts

Lines changed: 5 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ 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";
65

76
interface AuthenticatedMessage {
87
payload: WebSocketMessage;
@@ -42,10 +41,6 @@ export class AuthHandler {
4241

4342
// Emergency cleanup if too many nonces (DoS protection)
4443
if (this.usedNonces.size > this.MAX_NONCES) {
45-
logger.warn("Nonce storage exceeded limit, clearing all nonces", {
46-
type: "websocket_security",
47-
maxNonces: this.MAX_NONCES,
48-
});
4944
this.usedNonces.clear();
5045
}
5146
}
@@ -128,13 +123,9 @@ export class AuthHandler {
128123
console.log(`User ${ws.userId} authenticated via WebSocket`);
129124
}
130125
} catch (error: unknown) {
131-
logger.error(
132-
"WebSocket authentication failed",
133-
{ type: "websocket_auth_error" },
134-
error instanceof Error ? error : new Error(String(error))
135-
);
136-
137-
const isTokenExpired = (error as Record<string, unknown>)?.reason === "token-expired";
126+
const isTokenExpired =
127+
(error as Record<string, unknown>)?.reason === "token-expired" ||
128+
(error instanceof Error && error.message.includes("JWT is expired"));
138129

139130
ws.send(
140131
JSON.stringify({
@@ -166,31 +157,20 @@ export class AuthHandler {
166157
// 1. Timestamp validation (5-minute window + 1 minute tolerance for clock skew)
167158
const messageAge = Date.now() - timestamp;
168159
const MAX_MESSAGE_AGE = 5 * 60 * 1000; // 5 minutes
160+
// -60 seconds tolerance for clock skew
169161
if (messageAge > MAX_MESSAGE_AGE || messageAge < -60000) {
170-
// -60 seconds tolerance for clock skew
171-
logger.warn("Message rejected: timestamp out of range", {
172-
type: "websocket_security",
173-
messageAge,
174-
maxAge: MAX_MESSAGE_AGE,
175-
});
176162
return false;
177163
}
178164

179165
// 2. Check for replay attack using nonce
180166
const nonceKey = `${nonce}:${timestamp}`;
181167
if (this.usedNonces.has(nonceKey)) {
182-
logger.warn("Message rejected: nonce already used (replay attack)", {
183-
type: "websocket_security",
184-
});
185168
return false;
186169
}
187170

188171
try {
189172
// 3. Validate required parameters
190173
if (!jwtToken || !userId) {
191-
logger.warn("Missing JWT token or user ID for signature verification", {
192-
type: "websocket_security",
193-
});
194174
return false;
195175
}
196176

@@ -221,25 +201,13 @@ export class AuthHandler {
221201
const isValidStored = storedSecretSignature === signature;
222202
const isValid = isValidRegenerated || isValidStored;
223203

224-
if (!isValid) {
225-
logger.warn("Message signature verification failed", {
226-
type: "websocket_security",
227-
userId: userId || "unknown",
228-
});
229-
}
230-
231204
if (isValid) {
232205
// Mark nonce as used with current timestamp
233206
this.usedNonces.set(nonceKey, Date.now());
234207
}
235208

236209
return isValid;
237-
} catch (error) {
238-
logger.error(
239-
"Error verifying message signature",
240-
{ type: "websocket_auth_error" },
241-
error instanceof Error ? error : new Error(String(error))
242-
);
210+
} catch {
243211
return false;
244212
}
245213
}
@@ -258,9 +226,6 @@ export class AuthHandler {
258226
if (this.isAuthenticatedMessage(rawMessage)) {
259227
// This is an authenticated message, verify signature
260228
if (!ws.sessionSecret) {
261-
logger.warn("Authenticated message received but no session secret available", {
262-
type: "websocket_security",
263-
});
264229
return null;
265230
}
266231

@@ -271,10 +236,6 @@ export class AuthHandler {
271236
ws.userId
272237
);
273238
if (!isValid) {
274-
logger.warn("Message signature verification failed", {
275-
type: "websocket_security",
276-
userId: ws.userId || "unknown",
277-
});
278239
return null;
279240
}
280241

src/websocket/handlers/base.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ import { db, notes, folders } from "../../db";
22
import { eq, and } from "drizzle-orm";
33
import { AuthenticatedWebSocket, WebSocketMessage, ResourceOperationConfig } from "../types";
44
import { ConnectionManager } from "../middleware/connection-manager";
5-
import { logger } from "../../lib/logger";
6-
7-
const isDevelopment = process.env.NODE_ENV === "development";
85

96
export class BaseResourceHandler {
107
constructor(protected readonly _connectionManager: ConnectionManager) {}
@@ -66,16 +63,7 @@ export class BaseResourceHandler {
6663
);
6764
return;
6865
}
69-
} catch (error) {
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-
);
66+
} catch {
7967
ws.send(
8068
JSON.stringify({
8169
type: "error",
@@ -125,17 +113,6 @@ export class BaseResourceHandler {
125113
}
126114

127115
// Broadcast to user devices
128-
const sentCount = this._connectionManager.broadcastToUserDevices(ws.userId, syncMessage, ws);
129-
130-
if (isDevelopment) {
131-
logger.websocketEvent(
132-
`${config.resourceType}_${config.operation}`,
133-
ws.userId,
134-
undefined,
135-
resourceId,
136-
config.resourceType,
137-
`${sentCount}_devices`
138-
);
139-
}
116+
this._connectionManager.broadcastToUserDevices(ws.userId, syncMessage, ws);
140117
}
141118
}

0 commit comments

Comments
 (0)