Skip to content

Commit d3dba94

Browse files
Merge pull request #97 from embed-team/fix/repo-cleanup
fix: harden embed failure handling
2 parents 9f3d3c9 + 106b6df commit d3dba94

49 files changed

Lines changed: 2862 additions & 500 deletions

Some content is hidden

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

.moon/tasks/all.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ tasks:
99
args: "--check"
1010
lint:
1111
command: "oxlint --no-error-on-unmatched-pattern"
12+
inputs:
13+
- "**/*"
14+
- "/packages/config/oxlint.config.ts"
15+
- "/packages/config/oxlint/anti-slop/**/*"
16+
- "/packages/config/package.json"
17+
- "/pnpm-workspace.yaml"
18+
- "/pnpm-lock.yaml"
1219
lint/fix:
1320
extends: "lint"
1421
args: "--fix"

apps/api/src/index.ts

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
formatLog,
66
getErrorContext,
77
getRequestId,
8+
type LogContext,
89
} from "@embedly/logging";
910
import { Platforms } from "@embedly/platforms";
1011
import { httpInstrumentationMiddleware } from "@hono/otel";
@@ -21,6 +22,21 @@ import { version } from "../package.json";
2122

2223
type ScrapeResponse = Awaited<ReturnType<(typeof Platforms)[keyof typeof Platforms]["transform"]>>;
2324

25+
interface ApiLogContext extends LogContext {
26+
request_id: string;
27+
trace_id?: string;
28+
span_id?: string;
29+
source: string;
30+
platform: string;
31+
post_id: string;
32+
force: boolean;
33+
cache_status: "skipped" | "miss" | "hit" | "read_error" | "stored" | "write_error";
34+
outcome: "success" | "error";
35+
status_code: number;
36+
error_type?: string;
37+
duration_ms?: number;
38+
}
39+
2440
const config: ResolveConfigFn<CloudflareBindings> = (env) => {
2541
if (!env.OTEL_ENDPOINT) throw new Error("OTEL_ENDPOINT is required.");
2642

@@ -52,7 +68,7 @@ const app = new Hono<{ Bindings: CloudflareBindings }>()
5268
const { id, platform, force } = c.req.valid("json");
5369
const requestId = getRequestId(c.req.raw);
5470
const spanContext = trace.getActiveSpan()?.spanContext();
55-
const logContext: Record<string, unknown> = {
71+
const logContext: ApiLogContext = {
5672
request_id: requestId,
5773
trace_id: spanContext?.traceId,
5874
span_id: spanContext?.spanId,
@@ -73,25 +89,20 @@ const app = new Hono<{ Bindings: CloudflareBindings }>()
7389
const cachedItem = await cache.get<ScrapeResponse>(cacheKey, "json");
7490
if (cachedItem) {
7591
logContext.cache_status = "hit";
76-
return c.json(cachedItem as ScrapeResponse, 200);
92+
return c.json(cachedItem, 200);
7793
}
7894
} catch (cause) {
79-
const problem = createProblem(EmbedlyErrors.CacheReadFailed, {
80-
request_id: requestId,
81-
context: { ...logContext, ...getErrorContext(cause) },
82-
});
83-
Object.assign(logContext, getErrorContext(cause), {
84-
outcome: "error",
85-
status_code: problem.status,
86-
error_type: problem.type,
87-
});
88-
return c.json(problem, problem.status);
95+
logContext.cache_status = "read_error";
96+
console.warn(
97+
formatLog("warn", EmbedlyErrors.CacheReadFailed, {
98+
...logContext,
99+
...getErrorContext(cause),
100+
}),
101+
);
89102
}
90103
}
91104

92-
// oxlint-disable-next-line import/namespace
93-
const p = Platforms[platform as keyof typeof Platforms];
94-
if (!p) {
105+
if (!Object.hasOwn(Platforms, platform)) {
95106
const problem = createProblem(EmbedlyErrors.NoMatchesFound, {
96107
request_id: requestId,
97108
context: logContext,
@@ -104,6 +115,8 @@ const app = new Hono<{ Bindings: CloudflareBindings }>()
104115
});
105116
return c.json(problem, problem.status);
106117
}
118+
// oxlint-disable-next-line import/namespace -- SAFETY: Object.hasOwn verified this registry key.
119+
const p = Platforms[platform as keyof typeof Platforms];
107120

108121
let raw: unknown;
109122
try {
@@ -125,6 +138,7 @@ const app = new Hono<{ Bindings: CloudflareBindings }>()
125138

126139
let data: ScrapeResponse;
127140
try {
141+
// SAFETY: raw came from this platform's fetch implementation.
128142
data = await p.transform(raw as any);
129143
} catch (cause) {
130144
const problem = createProblem(EmbedlyErrors.PlatformTransformFailed, {
@@ -145,16 +159,13 @@ const app = new Hono<{ Bindings: CloudflareBindings }>()
145159
});
146160
logContext.cache_status = "stored";
147161
} catch (cause) {
148-
const problem = createProblem(EmbedlyErrors.CacheWriteFailed, {
149-
request_id: requestId,
150-
context: { ...logContext, ...getErrorContext(cause) },
151-
});
152-
Object.assign(logContext, getErrorContext(cause), {
153-
outcome: "error",
154-
status_code: problem.status,
155-
error_type: problem.type,
156-
});
157-
return c.json(problem, problem.status);
162+
logContext.cache_status = "write_error";
163+
console.warn(
164+
formatLog("warn", EmbedlyErrors.CacheWriteFailed, {
165+
...logContext,
166+
...getErrorContext(cause),
167+
}),
168+
);
158169
}
159170

160171
return c.json(data, 200);

apps/bot/compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ services:
1111
timeout: 1s
1212
retries: 30
1313
ports:
14-
- "6379:6379"
14+
- "127.0.0.1:6379:6379"
1515

1616
bot:
1717
container_name: embedly-bot

apps/bot/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
"devDependencies": {
3131
"@embedly/config": "workspace:*",
3232
"@types/node": "catalog:",
33-
"dotenv": "^17.4.2",
3433
"oxfmt": "catalog:",
3534
"oxlint": "catalog:",
3635
"tsdown": "^0.22.0",

apps/bot/src/commands/delete.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
EmbedlyLogs,
55
formatDiscordError,
66
getErrorContext,
7+
type LogContext,
78
} from "@embedly/logging";
89
import { Command } from "@sapphire/framework";
910
import {
@@ -32,6 +33,25 @@ const DELETE_SUCCESS_MESSAGES = [
3233
"🧹 all tidy! embed removed as requested~",
3334
];
3435

36+
interface DeleteLogContext extends LogContext {
37+
request_id: string;
38+
trace_id?: string;
39+
span_id?: string;
40+
source: "context_menu";
41+
interaction_id: string;
42+
channel_id: string | null;
43+
guild_id: string;
44+
user_id: string;
45+
message_id?: string;
46+
original_author_id?: string;
47+
has_manage_permission?: boolean;
48+
outcome: "success" | "error";
49+
status_code: number;
50+
error_type?: string;
51+
reason?: string;
52+
duration_ms?: number;
53+
}
54+
3555
export class DeleteCommand extends Command {
3656
public constructor(context: Command.LoaderContext, options: Command.Options) {
3757
super(context, {
@@ -62,7 +82,7 @@ export class DeleteCommand extends Command {
6282

6383
const startedAt = Date.now();
6484
const requestId = `context_menu:${interaction.id}`;
65-
const logContext: Record<string, unknown> = {
85+
const logContext: DeleteLogContext = {
6686
request_id: requestId,
6787
source: "context_menu",
6888
interaction_id: interaction.id,
@@ -196,7 +216,6 @@ export class DeleteCommand extends Command {
196216

197217
try {
198218
await msg.delete();
199-
await this.container.messageCache.removeBotMessage(msg.id);
200219
} catch (error) {
201220
const problem = createProblem(EmbedlyErrors.DeleteFailed, {
202221
request_id: requestId,
@@ -216,6 +235,20 @@ export class DeleteCommand extends Command {
216235
return;
217236
}
218237

238+
try {
239+
await this.container.messageCache.removeBotMessage(msg.id);
240+
} catch (error) {
241+
botErrors.add(1, {
242+
...metricContext,
243+
error_type: EmbedlyErrors.MessageCacheFailed.type,
244+
});
245+
log("warn", EmbedlyErrors.MessageCacheFailed, {
246+
...logContext,
247+
error_type: EmbedlyErrors.MessageCacheFailed.type,
248+
...getErrorContext(error),
249+
});
250+
}
251+
219252
await interaction.editReply(
220253
DELETE_SUCCESS_MESSAGES[~~(DELETE_SUCCESS_MESSAGES.length * Math.random())],
221254
);

0 commit comments

Comments
 (0)