@theholocron/holocron-plugin-sentry 5.0.0-alpha.228 → 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 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.228",
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.228"
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.228"
56
+ "@theholocron/cli": "5.0.0-alpha.229"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "@theholocron/sentry-client": {}