@theholocron/holocron-plugin-sentry 5.0.0-alpha.227 → 5.0.0-alpha.229
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +156 -0
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -82,6 +82,7 @@ declare function createContext(options: SentryPluginOptions): PluginContext;
|
|
|
82
82
|
declare function errors(ctx: PluginContext): Errors;
|
|
83
83
|
declare function createPlugin(options: SentryPluginOptions): {
|
|
84
84
|
name: string;
|
|
85
|
+
probes: import("@theholocron/cli").PluginProbes;
|
|
85
86
|
capabilities: {
|
|
86
87
|
errors: () => Errors;
|
|
87
88
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AuthError, ProviderApiError, createFeatureResolver, createResolveToken } from "@theholocron/cli";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { createSentryClient } from "@theholocron/sentry-client";
|
|
3
4
|
import { errorMessage } from "@theholocron/misc-utils";
|
|
4
5
|
//#region src/auth.ts
|
|
@@ -300,6 +301,156 @@ function toSlug(name) {
|
|
|
300
301
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
301
302
|
}
|
|
302
303
|
//#endregion
|
|
304
|
+
//#region src/probes/probe-deps.type.ts
|
|
305
|
+
function requireOrg(deps) {
|
|
306
|
+
if (!deps.org) throw new Error("the Sentry probes need `org` in the sentry provider's options");
|
|
307
|
+
return deps.org;
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/probes/integrations.ts
|
|
311
|
+
/** Where a webhook points, without a query string that could carry a token. */
|
|
312
|
+
function where(url) {
|
|
313
|
+
if (!url) return "no webhook";
|
|
314
|
+
try {
|
|
315
|
+
const u = new URL(url);
|
|
316
|
+
return `${u.host}${u.pathname}`;
|
|
317
|
+
} catch {
|
|
318
|
+
return "an unreadable URL";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function integrationsProbe(deps) {
|
|
322
|
+
return {
|
|
323
|
+
summary: "List the custom integrations: scopes, alert action, webhook, and whether the client secret is visible.",
|
|
324
|
+
writes: false,
|
|
325
|
+
async run(probe) {
|
|
326
|
+
const apps = await deps.client().sentryApps.list(requireOrg(deps));
|
|
327
|
+
if (apps.length === 0) probe.say({
|
|
328
|
+
status: "info",
|
|
329
|
+
label: "no custom integrations"
|
|
330
|
+
});
|
|
331
|
+
for (const app of apps) {
|
|
332
|
+
probe.secret(app.clientSecret);
|
|
333
|
+
probe.say({
|
|
334
|
+
status: "ok",
|
|
335
|
+
label: `${app.name} (${app.slug})`,
|
|
336
|
+
detail: [
|
|
337
|
+
`scopes: ${app.scopes.join(", ") || "none"}`,
|
|
338
|
+
`alert action: ${app.isAlertable ? "on" : "off"}`,
|
|
339
|
+
`webhook: ${where(app.webhookUrl)}`,
|
|
340
|
+
`secret visible: ${app.clientSecret ? "yes" : "no"}`
|
|
341
|
+
].join(" | ")
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region src/probes/rules.ts
|
|
349
|
+
function rulesProbe(deps) {
|
|
350
|
+
return {
|
|
351
|
+
summary: "List the alert rules: enabled, projects attached, and what each action sends to.",
|
|
352
|
+
writes: false,
|
|
353
|
+
async run(probe) {
|
|
354
|
+
const org = requireOrg(deps);
|
|
355
|
+
const client = deps.client();
|
|
356
|
+
const [workflows, apps] = await Promise.all([client.workflows.list(org), client.sentryApps.list(org)]);
|
|
357
|
+
const alertable = new Set(apps.filter((a) => a.isAlertable).map((a) => a.slug));
|
|
358
|
+
if (workflows.length === 0) probe.say({
|
|
359
|
+
status: "info",
|
|
360
|
+
label: "no alert rules"
|
|
361
|
+
});
|
|
362
|
+
for (const wf of workflows) {
|
|
363
|
+
const actions = wf.actionFilters.flatMap((f) => f.actions);
|
|
364
|
+
const described = actions.map((a) => {
|
|
365
|
+
const target = String(a.config["targetIdentifier"] ?? a.config["targetType"] ?? "");
|
|
366
|
+
return target ? `${a.type} ${target}` : a.type;
|
|
367
|
+
});
|
|
368
|
+
const problems = actions.flatMap((a) => {
|
|
369
|
+
if (a.type === "sentry_app") return [`a sentry_app action shows as an unknown integration`];
|
|
370
|
+
if (a.type === "webhook" && !alertable.has(String(a.config["targetIdentifier"]))) return [`webhook ${String(a.config["targetIdentifier"])} is not an alertable integration`];
|
|
371
|
+
return [];
|
|
372
|
+
});
|
|
373
|
+
probe.say({
|
|
374
|
+
status: problems.length > 0 || !wf.enabled ? "fail" : "ok",
|
|
375
|
+
label: wf.name,
|
|
376
|
+
detail: [
|
|
377
|
+
wf.enabled ? "enabled" : "DISABLED",
|
|
378
|
+
`detectors: ${wf.detectorIds.length}`,
|
|
379
|
+
`actions: ${described.join(", ") || "none"}`,
|
|
380
|
+
...problems
|
|
381
|
+
].join(" | ")
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region src/probes/send-error.ts
|
|
389
|
+
/** Which DSN to send to: `SENTRY_DSN`, else the first client key of the project named in `HOLOCRON_SENTRY_PROBE_PROJECT`. */
|
|
390
|
+
async function dsnFor(deps) {
|
|
391
|
+
const env = deps.env ?? process.env;
|
|
392
|
+
if (env["SENTRY_DSN"]) return env["SENTRY_DSN"];
|
|
393
|
+
const project = env["HOLOCRON_SENTRY_PROBE_PROJECT"];
|
|
394
|
+
if (!project) throw new Error("set SENTRY_DSN, or HOLOCRON_SENTRY_PROBE_PROJECT to a project slug, to say where to send the error");
|
|
395
|
+
const dsn = (await deps.client().projects.keys(requireOrg(deps), project))[0]?.dsn.public;
|
|
396
|
+
if (!dsn) throw new Error(`project \`${project}\` has no client key`);
|
|
397
|
+
return dsn;
|
|
398
|
+
}
|
|
399
|
+
function sendErrorProbe(deps) {
|
|
400
|
+
return {
|
|
401
|
+
summary: "Send one labelled test error to a project, to follow it through the alert rule to Discord.",
|
|
402
|
+
writes: true,
|
|
403
|
+
async run(probe) {
|
|
404
|
+
const dsn = new URL(await dsnFor(deps));
|
|
405
|
+
probe.secret(dsn.username);
|
|
406
|
+
const projectId = dsn.pathname.replace(/^\/+/, "");
|
|
407
|
+
const eventId = randomUUID().replaceAll("-", "");
|
|
408
|
+
const message = `Holocron probe: sent on purpose (${(/* @__PURE__ */ new Date()).toISOString()})`;
|
|
409
|
+
const envelope = [
|
|
410
|
+
JSON.stringify({
|
|
411
|
+
event_id: eventId,
|
|
412
|
+
sent_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
413
|
+
}),
|
|
414
|
+
JSON.stringify({ type: "event" }),
|
|
415
|
+
JSON.stringify({
|
|
416
|
+
event_id: eventId,
|
|
417
|
+
level: "error",
|
|
418
|
+
platform: "node",
|
|
419
|
+
environment: "holocron-probe",
|
|
420
|
+
message: { formatted: message },
|
|
421
|
+
exception: { values: [{
|
|
422
|
+
type: "HolocronProbeError",
|
|
423
|
+
value: message
|
|
424
|
+
}] },
|
|
425
|
+
tags: { probe: "sentry.send-error" }
|
|
426
|
+
})
|
|
427
|
+
].join("\n");
|
|
428
|
+
if (await probe.write("send a test error", async () => {
|
|
429
|
+
const res = await (deps.fetch ?? fetch)(`${dsn.protocol}//${dsn.host}/api/${projectId}/envelope/?sentry_version=7&sentry_key=${dsn.username}&sentry_client=holocron-probe/1`, {
|
|
430
|
+
method: "POST",
|
|
431
|
+
headers: { "content-type": "application/x-sentry-envelope" },
|
|
432
|
+
body: envelope
|
|
433
|
+
});
|
|
434
|
+
if (!res.ok) throw new Error(`Sentry answered ${res.status}`);
|
|
435
|
+
return true;
|
|
436
|
+
})) probe.say({
|
|
437
|
+
status: "ok",
|
|
438
|
+
label: "sent HolocronProbeError",
|
|
439
|
+
detail: "look for it in the project's issues and, through the rule, in #errors; resolve the issue by hand (a probe cannot delete it)"
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/probes/index.ts
|
|
446
|
+
function sentryProbes(deps) {
|
|
447
|
+
return {
|
|
448
|
+
integrations: integrationsProbe(deps),
|
|
449
|
+
rules: rulesProbe(deps),
|
|
450
|
+
"send-error": sendErrorProbe(deps)
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
//#endregion
|
|
303
454
|
//#region src/verify-token.ts
|
|
304
455
|
async function verifyToken(token, opts = {}) {
|
|
305
456
|
const client = createSentryClient({
|
|
@@ -348,6 +499,11 @@ function createPlugin(options) {
|
|
|
348
499
|
const ctx = createContext(options);
|
|
349
500
|
return {
|
|
350
501
|
name: "@theholocron/holocron-plugin-sentry",
|
|
502
|
+
probes: sentryProbes({
|
|
503
|
+
org: options.org,
|
|
504
|
+
client: ctx.relayClient,
|
|
505
|
+
fetch: options.fetch
|
|
506
|
+
}),
|
|
351
507
|
capabilities: { errors: () => errors(ctx) }
|
|
352
508
|
};
|
|
353
509
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/holocron-plugin-sentry",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.229",
|
|
4
4
|
"description": "Holocron plugin for Sentry. Implements the errors capability against Sentry's management API — project provisioning and DSN retrieval.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"errors",
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"tsx": "4.23.15",
|
|
49
49
|
"typescript": "^5.9.3",
|
|
50
50
|
"vitest": "^4.1.11",
|
|
51
|
-
"@theholocron/cli": "5.0.0-alpha.
|
|
51
|
+
"@theholocron/cli": "5.0.0-alpha.229"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@theholocron/misc-utils": "^1.16.0",
|
|
55
55
|
"@theholocron/sentry-client": "^1.54.0",
|
|
56
|
-
"@theholocron/cli": "5.0.0-alpha.
|
|
56
|
+
"@theholocron/cli": "5.0.0-alpha.229"
|
|
57
57
|
},
|
|
58
58
|
"peerDependenciesMeta": {
|
|
59
59
|
"@theholocron/sentry-client": {}
|