@theholocron/holocron-plugin-sentry 5.0.0-alpha.23 → 5.0.0-alpha.231

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
@@ -1,7 +1,17 @@
1
- import { AuthError, Errors, ResolveTokenInput } from "@theholocron/cli";
1
+ import { AuthError, EnsureRelayInput, EnsureRelayResult, Errors, ResolveTokenInput, VerifyTokenFailure, VerifyTokenResult, VerifyTokenResult as VerifyTokenResult$1, VerifyTokenSuccess } from "@theholocron/cli";
2
2
  import { SentryClient, SentryClientOptions, createSentryClient } from "@theholocron/sentry-client";
3
3
  //#region src/auth.d.ts
4
4
  declare const resolveToken: (input?: ResolveTokenInput) => string;
5
+ /**
6
+ * The token for the Comlink relay: an integration, its client secret and an alert rule need more than the general token
7
+ * (`alerts:write`, `org:write`, `event:read`), so it has its own.
8
+ */
9
+ declare const resolveComlinkToken: (input?: ResolveTokenInput) => string;
10
+ /**
11
+ * The token for the relay: the Comlink one when there is one, else the general Sentry token as the last resort. The
12
+ * fallback only helps if the general token happens to carry the scopes; otherwise Sentry's answer names what is missing.
13
+ */
14
+ declare function resolveRelayToken(input?: ResolveTokenInput): string;
5
15
  //#endregion
6
16
  //#region src/capabilities/errors.d.ts
7
17
  interface SentryErrorsOptions {
@@ -13,16 +23,21 @@ interface SentryErrorsOptions {
13
23
  declare class SentryErrors implements Errors {
14
24
  private readonly client;
15
25
  private readonly opts;
26
+ /** The client for the relay, made with the Comlink token (see `resolveRelayToken`). */
27
+ private readonly relayClient;
16
28
  readonly key: "errors";
17
29
  readonly providerName = "sentry";
18
- constructor(client: () => SentryClient, opts: SentryErrorsOptions);
30
+ constructor(client: () => SentryClient, opts: SentryErrorsOptions,
31
+ /** The client for the relay, made with the Comlink token (see `resolveRelayToken`). */
32
+ relayClient?: () => SentryClient);
33
+ ensureRelay(input: EnsureRelayInput): Promise<EnsureRelayResult>;
19
34
  describe(): Promise<{
20
35
  provider: string;
21
36
  envKeys: string[];
22
37
  }>;
23
38
  whoami(): Promise<{
24
39
  org: string;
25
- }>;
40
+ } | null>;
26
41
  ensureProject(input: {
27
42
  name: string;
28
43
  platform?: string;
@@ -39,20 +54,11 @@ declare class SentryErrors implements Errors {
39
54
  }
40
55
  //#endregion
41
56
  //#region src/verify-token.d.ts
42
- interface VerifyTokenSuccess {
43
- ok: true;
44
- subject: string;
45
- }
46
- interface VerifyTokenFailure {
47
- ok: false;
48
- message: string;
49
- }
50
- type VerifyTokenResult = VerifyTokenSuccess | VerifyTokenFailure;
51
57
  interface VerifyTokenOptions {
52
58
  baseUrl?: string;
53
59
  fetch?: typeof fetch;
54
60
  }
55
- declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<VerifyTokenResult>;
61
+ declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<VerifyTokenResult$1>;
56
62
  //#endregion
57
63
  //#region src/index.d.ts
58
64
  interface SentryPluginOptions extends ResolveTokenInput, SentryErrorsOptions {
@@ -69,15 +75,18 @@ interface SentryPluginOptions extends ResolveTokenInput, SentryErrorsOptions {
69
75
  interface PluginContext {
70
76
  options: SentryPluginOptions;
71
77
  client: () => SentryClient;
78
+ /** The client for the Comlink relay: its own token (`sentry.comlink`), the general one only as a last resort. */
79
+ relayClient: () => SentryClient;
72
80
  }
73
81
  declare function createContext(options: SentryPluginOptions): PluginContext;
74
82
  declare function errors(ctx: PluginContext): Errors;
75
83
  declare function createPlugin(options: SentryPluginOptions): {
76
84
  name: string;
85
+ probes: import("@theholocron/cli").PluginProbes;
77
86
  capabilities: {
78
87
  errors: () => Errors;
79
88
  };
80
89
  };
81
90
  declare const AUTH_HINT: string;
82
91
  //#endregion
83
- export { AUTH_HINT, AuthError, PluginContext, type ResolveTokenInput, type SentryClient, type SentryClientOptions, SentryErrors, type SentryErrorsOptions, SentryPluginOptions, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, createContext, createPlugin, createSentryClient, errors, resolveToken, verifyToken };
92
+ export { AUTH_HINT, AuthError, PluginContext, type ResolveTokenInput, type SentryClient, type SentryClientOptions, SentryErrors, type SentryErrorsOptions, SentryPluginOptions, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, createContext, createPlugin, createSentryClient, errors, resolveComlinkToken, resolveRelayToken, resolveToken, verifyToken };
package/dist/index.mjs CHANGED
@@ -1,5 +1,7 @@
1
- import { AuthError, ProviderApiError, createResolveToken } from "@theholocron/cli";
1
+ import { AuthError, ProviderApiError, createFeatureResolver, createResolveToken } from "@theholocron/cli";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { createSentryClient } from "@theholocron/sentry-client";
4
+ import { errorMessage } from "@theholocron/misc-utils";
3
5
  //#region src/auth.ts
4
6
  const resolveToken = createResolveToken({
5
7
  envName: "HOLOCRON_SENTRY_TOKEN",
@@ -7,16 +9,245 @@ const resolveToken = createResolveToken({
7
9
  keyringService: "sentry",
8
10
  errorMessage: "no Sentry auth token found. Pass --token <TOKEN>, set HOLOCRON_SENTRY_TOKEN / SENTRY_AUTH_TOKEN, or run: holocron auth set sentry <TOKEN>"
9
11
  });
12
+ /**
13
+ * The token for the Comlink relay: an integration, its client secret and an alert rule need more than the general token
14
+ * (`alerts:write`, `org:write`, `event:read`), so it has its own.
15
+ */
16
+ const resolveComlinkToken = createFeatureResolver({
17
+ envName: "HOLOCRON_SENTRY_COMLINK_TOKEN",
18
+ keyringKey: "sentry.comlink",
19
+ vendor: "Sentry"
20
+ });
21
+ /**
22
+ * The token for the relay: the Comlink one when there is one, else the general Sentry token as the last resort. The
23
+ * fallback only helps if the general token happens to carry the scopes; otherwise Sentry's answer names what is missing.
24
+ */
25
+ function resolveRelayToken(input = {}) {
26
+ try {
27
+ return resolveComlinkToken(input);
28
+ } catch (err) {
29
+ if (!(err instanceof AuthError)) throw err;
30
+ return resolveToken(input);
31
+ }
32
+ }
33
+ //#endregion
34
+ //#region src/capabilities/relay.ts
35
+ /** What the integration needs to read: the alert's event. It is also why reading its secret needs the same scope. */
36
+ const INTEGRATION_SCOPES = ["event:read"];
37
+ const DEFAULT_FREQUENCY = 5;
38
+ /**
39
+ * The alert action that sends through an integration, in the shape Sentry's own page writes when "Send a notification via
40
+ * an integration" is picked: a `webhook` action named by the integration's slug. (A `sentry_app` action with the numeric
41
+ * id is accepted too, but the page shows it as an unknown integration.)
42
+ */
43
+ const integrationAction = (slug) => ({
44
+ type: "webhook",
45
+ config: {
46
+ targetType: null,
47
+ targetDisplay: null,
48
+ targetIdentifier: slug
49
+ },
50
+ data: {}
51
+ });
52
+ const sendsThrough = (action, slug) => action.type === "webhook" && action.config["targetIdentifier"] === slug;
53
+ /** The `sentry_app` form an earlier version wrote for this integration, which the page cannot name. */
54
+ const isLegacyAction = (action, appId) => action.type === "sentry_app" && String(action.config["targetIdentifier"]) === String(appId);
55
+ /** A workflow as Sentry returned it, as the body to send back (the nested ids are not part of an update). */
56
+ function toInput(wf) {
57
+ return {
58
+ name: wf.name,
59
+ enabled: wf.enabled,
60
+ environment: wf.environment,
61
+ config: wf.config,
62
+ detectorIds: wf.detectorIds,
63
+ triggers: {
64
+ logicType: wf.triggers.logicType,
65
+ conditions: wf.triggers.conditions.map((c) => ({
66
+ type: c.type,
67
+ comparison: c.comparison,
68
+ conditionResult: c.conditionResult
69
+ }))
70
+ },
71
+ actionFilters: wf.actionFilters.map((f) => ({
72
+ logicType: f.logicType,
73
+ conditions: f.conditions.map((c) => ({
74
+ type: c.type,
75
+ comparison: c.comparison,
76
+ conditionResult: c.conditionResult
77
+ })),
78
+ actions: f.actions.map((a) => ({
79
+ type: a.type,
80
+ config: a.config,
81
+ data: a.data
82
+ }))
83
+ }))
84
+ };
85
+ }
86
+ /** Sentry's own "high priority issues" rule, which is the default for a new alert. */
87
+ function newWorkflow(name, frequency, detectorIds, slug) {
88
+ return {
89
+ name,
90
+ enabled: true,
91
+ environment: null,
92
+ config: { frequency },
93
+ detectorIds,
94
+ triggers: {
95
+ logicType: "any-short",
96
+ conditions: [{
97
+ type: "new_high_priority_issue",
98
+ comparison: true,
99
+ conditionResult: true
100
+ }, {
101
+ type: "existing_high_priority_issue",
102
+ comparison: true,
103
+ conditionResult: true
104
+ }]
105
+ },
106
+ actionFilters: [{
107
+ logicType: "all",
108
+ conditions: [],
109
+ actions: [integrationAction(slug)]
110
+ }]
111
+ };
112
+ }
113
+ /** The integration: found by name, made if missing, and its webhook and alert action brought in line. */
114
+ async function ensureIntegration(client, org, input) {
115
+ const existing = (await client.sentryApps.list(org)).find((a) => a.name === input.name);
116
+ if (!existing) return {
117
+ app: await client.sentryApps.create(org, {
118
+ name: input.name,
119
+ scopes: INTEGRATION_SCOPES,
120
+ webhookUrl: input.webhookUrl,
121
+ isAlertable: true
122
+ }),
123
+ created: true,
124
+ updated: []
125
+ };
126
+ const updated = [];
127
+ if (existing.webhookUrl !== input.webhookUrl) updated.push("webhook URL");
128
+ if (!existing.isAlertable) updated.push("alert action");
129
+ const scopes = [.../* @__PURE__ */ new Set([...existing.scopes, ...INTEGRATION_SCOPES])];
130
+ if (scopes.length !== existing.scopes.length) updated.push("scopes");
131
+ if (updated.length === 0) return {
132
+ app: existing,
133
+ created: false,
134
+ updated
135
+ };
136
+ return {
137
+ app: await client.sentryApps.update(existing.slug, {
138
+ name: existing.name,
139
+ scopes,
140
+ webhookUrl: input.webhookUrl,
141
+ isAlertable: true
142
+ }),
143
+ created: false,
144
+ updated
145
+ };
146
+ }
147
+ /** The issue stream detector of each project, so a wrong project fails before anything is changed. */
148
+ async function detectorsFor(client, org, slugs) {
149
+ const [projects, detectors] = await Promise.all([client.projects.list(org), client.workflows.detectors(org)]);
150
+ return slugs.map((slug) => {
151
+ const project = projects.find((p) => p.slug === slug);
152
+ if (!project) throw new Error(`Sentry project \`${slug}\` was not found in ${org}`);
153
+ const detector = detectors.find((d) => d.projectId === project.id && d.type === "issue_stream");
154
+ if (!detector) throw new Error(`Sentry project \`${slug}\` has no issue stream detector to attach an alert to`);
155
+ return detector.id;
156
+ });
157
+ }
158
+ /** The alert rule: one workflow for every project, found by name, made if missing, reconciled if not. */
159
+ async function ensureRule(client, org, input, install, detectorIds) {
160
+ const workflows = await client.workflows.list(org);
161
+ const frequency = input.rule.frequency ?? DEFAULT_FREQUENCY;
162
+ const existing = workflows.find((w) => w.name === input.rule.name);
163
+ if (!existing) {
164
+ await client.workflows.create(org, newWorkflow(input.rule.name, frequency, detectorIds, install.slug));
165
+ return {
166
+ created: true,
167
+ updated: []
168
+ };
169
+ }
170
+ const next = toInput(existing);
171
+ const updated = [];
172
+ if (!existing.enabled) {
173
+ next.enabled = true;
174
+ updated.push("enabled");
175
+ }
176
+ if (input.rule.frequency !== void 0 && existing.config.frequency !== input.rule.frequency) {
177
+ next.config = {
178
+ ...existing.config,
179
+ frequency: input.rule.frequency
180
+ };
181
+ updated.push("frequency");
182
+ }
183
+ const missing = detectorIds.filter((id) => !existing.detectorIds.includes(id));
184
+ if (missing.length > 0) {
185
+ next.detectorIds = [...existing.detectorIds, ...missing];
186
+ updated.push("projects");
187
+ }
188
+ const sends = existing.actionFilters.some((f) => f.actions.some((a) => sendsThrough(a, install.slug)));
189
+ const legacy = existing.actionFilters.some((f) => f.actions.some((a) => isLegacyAction(a, install.appId)));
190
+ if (legacy) for (const f of next.actionFilters) f.actions = f.actions.filter((a) => !isLegacyAction(a, install.appId));
191
+ if (!sends) {
192
+ const open = next.actionFilters.find((f) => f.conditions.length === 0);
193
+ if (open) open.actions.push(integrationAction(install.slug));
194
+ else next.actionFilters.push({
195
+ logicType: "all",
196
+ conditions: [],
197
+ actions: [integrationAction(install.slug)]
198
+ });
199
+ }
200
+ if (legacy || !sends) updated.push("action");
201
+ if (updated.length === 0) return {
202
+ created: false,
203
+ updated
204
+ };
205
+ await client.workflows.update(org, existing.id, next);
206
+ return {
207
+ created: false,
208
+ updated
209
+ };
210
+ }
211
+ /** The signing secret, if the token may see it: a new integration's answer omits it, so it is read again. */
212
+ async function secretOf(client, app) {
213
+ if (app.clientSecret) return app.clientSecret;
214
+ return (await client.sentryApps.get(app.slug)).clientSecret || void 0;
215
+ }
216
+ async function ensureSentryRelay(client, org, input) {
217
+ if (input.projects.length === 0) throw new Error("relay needs at least one Sentry project to alert on");
218
+ const detectorIds = await detectorsFor(client, org, input.projects);
219
+ const integration = await ensureIntegration(client, org, input);
220
+ const install = (await client.sentryApps.installations(org)).find((i) => i.app.slug === integration.app.slug);
221
+ if (!install) throw new Error(`the integration \`${integration.app.name}\` is not installed on ${org}`);
222
+ const rule = await ensureRule(client, org, input, {
223
+ slug: install.app.slug,
224
+ appId: install.app.sentryAppId
225
+ }, detectorIds);
226
+ const clientSecret = await secretOf(client, integration.app);
227
+ return {
228
+ integration: {
229
+ created: integration.created,
230
+ updated: integration.updated
231
+ },
232
+ rule,
233
+ ...clientSecret ? { clientSecret } : {}
234
+ };
235
+ }
10
236
  //#endregion
11
237
  //#region src/capabilities/errors.ts
12
238
  var SentryErrors = class {
13
239
  client;
14
240
  opts;
241
+ relayClient;
15
242
  key = "errors";
16
243
  providerName = "sentry";
17
- constructor(client, opts) {
244
+ constructor(client, opts, relayClient = client) {
18
245
  this.client = client;
19
246
  this.opts = opts;
247
+ this.relayClient = relayClient;
248
+ }
249
+ async ensureRelay(input) {
250
+ return ensureSentryRelay(this.relayClient(), this.requireOrg(), input);
20
251
  }
21
252
  async describe() {
22
253
  return {
@@ -25,7 +256,8 @@ var SentryErrors = class {
25
256
  };
26
257
  }
27
258
  async whoami() {
28
- return { org: (await this.client().auth.getOrg(this.requireOrg())).slug };
259
+ if (!this.opts.org) return null;
260
+ return { org: (await this.client().auth.getOrg(this.opts.org)).slug };
29
261
  }
30
262
  async ensureProject(input) {
31
263
  const org = this.requireOrg();
@@ -69,6 +301,156 @@ function toSlug(name) {
69
301
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
70
302
  }
71
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
72
454
  //#region src/verify-token.ts
73
455
  async function verifyToken(token, opts = {}) {
74
456
  const client = createSentryClient({
@@ -84,7 +466,7 @@ async function verifyToken(token, opts = {}) {
84
466
  } catch (err) {
85
467
  return {
86
468
  ok: false,
87
- message: err instanceof Error ? err.message : String(err)
469
+ message: errorMessage(err)
88
470
  };
89
471
  }
90
472
  }
@@ -92,12 +474,18 @@ async function verifyToken(token, opts = {}) {
92
474
  //#region src/index.ts
93
475
  function createContext(options) {
94
476
  let client;
477
+ let relayClient;
95
478
  return {
96
479
  options,
97
480
  client: () => client ??= createSentryClient({
98
481
  token: resolveToken(options),
99
482
  baseUrl: options.baseUrl,
100
483
  fetch: options.fetch
484
+ }),
485
+ relayClient: () => relayClient ??= createSentryClient({
486
+ token: resolveRelayToken(options),
487
+ baseUrl: options.baseUrl,
488
+ fetch: options.fetch
101
489
  })
102
490
  };
103
491
  }
@@ -105,15 +493,20 @@ function errors(ctx) {
105
493
  return new SentryErrors(ctx.client, {
106
494
  org: ctx.options.org,
107
495
  team: ctx.options.team
108
- });
496
+ }, ctx.relayClient);
109
497
  }
110
498
  function createPlugin(options) {
111
499
  const ctx = createContext(options);
112
500
  return {
113
501
  name: "@theholocron/holocron-plugin-sentry",
502
+ probes: sentryProbes({
503
+ org: options.org,
504
+ client: ctx.relayClient,
505
+ fetch: options.fetch
506
+ }),
114
507
  capabilities: { errors: () => errors(ctx) }
115
508
  };
116
509
  }
117
510
  const AUTH_HINT = "generate an auth token at https://sentry.io/settings/account/api/auth-tokens/ with project:read, project:write, and org:read scopes, then run: holocron auth set sentry <TOKEN>";
118
511
  //#endregion
119
- export { AUTH_HINT, AuthError, SentryErrors, createContext, createPlugin, createSentryClient, errors, resolveToken, verifyToken };
512
+ export { AUTH_HINT, AuthError, SentryErrors, createContext, createPlugin, createSentryClient, errors, resolveComlinkToken, resolveRelayToken, resolveToken, verifyToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/holocron-plugin-sentry",
3
- "version": "5.0.0-alpha.23",
3
+ "version": "5.0.0-alpha.231",
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",
@@ -8,12 +8,12 @@
8
8
  "plugin",
9
9
  "sentry"
10
10
  ],
11
- "homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-sentry#readme",
11
+ "homepage": "https://github.com/theholocron/holocron/tree/main/plugins/holocron-plugin-sentry#readme",
12
12
  "bugs": "https://github.com/theholocron/holocron/issues",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git+https://github.com/theholocron/holocron.git",
16
- "directory": "packages/holocron-plugin-sentry"
16
+ "directory": "plugins/holocron-plugin-sentry"
17
17
  },
18
18
  "license": "MIT",
19
19
  "author": "Newton Koumantzelis",
@@ -30,12 +30,13 @@
30
30
  "dist"
31
31
  ],
32
32
  "devDependencies": {
33
- "@theholocron/eslint-config": "^8.0.0",
34
- "@theholocron/http-client": "^1.22.0",
35
- "@theholocron/sentry-client": "^1.22.0",
36
- "@theholocron/tsconfig": "^8.0.0",
37
- "@theholocron/tsdown-config": "^8.0.0",
38
- "@theholocron/vitest-config": "^8.0.0",
33
+ "@theholocron/eslint-config": "^8.8.2",
34
+ "@theholocron/http-client": "^1.54.2",
35
+ "@theholocron/misc-utils": "^1.16.0",
36
+ "@theholocron/sentry-client": "^1.54.0",
37
+ "@theholocron/tsconfig": "^8.8.2",
38
+ "@theholocron/tsdown-config": "^8.8.2",
39
+ "@theholocron/vitest-config": "^8.8.2",
39
40
  "@types/node": "^26",
40
41
  "@vitest/coverage-v8": "^4.1.11",
41
42
  "@vitest/eslint-plugin": "^1.6.27",
@@ -44,14 +45,15 @@
44
45
  "eslint-plugin-n": "^18.3.0",
45
46
  "globals": "^17.11.0",
46
47
  "tsdown": "^0.22.14",
47
- "tsx": "4.23.12",
48
+ "tsx": "4.23.15",
48
49
  "typescript": "^5.9.3",
49
50
  "vitest": "^4.1.11",
50
- "@theholocron/cli": "5.0.0-alpha.23"
51
+ "@theholocron/cli": "5.0.0-alpha.231"
51
52
  },
52
53
  "peerDependencies": {
53
- "@theholocron/sentry-client": "^1.22.0",
54
- "@theholocron/cli": "5.0.0-alpha.23"
54
+ "@theholocron/misc-utils": "^1.16.0",
55
+ "@theholocron/sentry-client": "^1.54.0",
56
+ "@theholocron/cli": "5.0.0-alpha.231"
55
57
  },
56
58
  "peerDependenciesMeta": {
57
59
  "@theholocron/sentry-client": {}