@sentry/junior-sentry 0.265.0 → 0.266.0

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/README.md CHANGED
@@ -52,6 +52,8 @@ Junior verifies `Sentry-Hook-Signature`, reads the org slug from the signed payl
52
52
  - `issue.created` for `org/project#issueId` and `org/project`.
53
53
  - `metric_alert.critical`, `metric_alert.warning`, and `metric_alert.resolved` for `metric-alert:org/alertRuleId` and for `org/project` of each project on the alert rule.
54
54
 
55
+ Each event has trusted data from Sentry. Issue events have `issueId`, `level`, `priority`, `issueCategory`, `issueType`, and `url` when Sentry sends them. Metric alert events have `alertRuleId`, `incidentId`, and `url`. On `org/project`, watches and event automations can filter issue events on `level`, `priority`, and `issueCategory`, and metric alert events on `alertRuleId`. The issue title, culprit, and alert description stay untrusted text.
56
+
55
57
  Create watches/event automations before the webhook arrives; unmatched deliveries are not replayed.
56
58
 
57
59
  Full guide: https://junior.sentry.dev/extend/sentry-plugin/
@@ -1,5 +1,60 @@
1
+ // src/events/issue.ts
2
+ import {
3
+ eventMatchFieldsSchema as eventMatchFieldsSchema2
4
+ } from "@sentry/junior-plugin-api";
5
+
6
+ // src/events/scope.ts
7
+ import {
8
+ eventMatchFieldsSchema
9
+ } from "@sentry/junior-plugin-api";
10
+ import { z } from "zod";
11
+ var sentrySlugDataSchema = z.string().min(1).toLowerCase();
12
+ function sentryProjectsMatchFields(events) {
13
+ return eventMatchFieldsSchema.parse({
14
+ projects: {
15
+ kind: "string",
16
+ list: true,
17
+ description: "lowercase Sentry project slugs, such as junior; an issue has one, a metric alert has the projects of its rule",
18
+ events
19
+ }
20
+ });
21
+ }
22
+
1
23
  // src/events/issue.ts
2
24
  var SENTRY_ISSUE_EVENTS = ["issue.created"];
25
+ var SENTRY_ISSUE_LEVELS = [
26
+ "fatal",
27
+ "error",
28
+ "warning",
29
+ "info",
30
+ "debug"
31
+ ];
32
+ var SENTRY_ISSUE_PRIORITIES = ["high", "medium", "low"];
33
+ var SENTRY_ISSUE_MATCH_FIELDS = eventMatchFieldsSchema2.parse({
34
+ ...sentryProjectsMatchFields(SENTRY_ISSUE_EVENTS),
35
+ issueId: {
36
+ kind: "string",
37
+ description: "numeric Sentry issue id as text, such as 7513266773",
38
+ events: [...SENTRY_ISSUE_EVENTS]
39
+ },
40
+ level: {
41
+ kind: "string",
42
+ description: "issue level: fatal, error, warning, info, or debug",
43
+ enum: [...SENTRY_ISSUE_LEVELS],
44
+ events: [...SENTRY_ISSUE_EVENTS]
45
+ },
46
+ priority: {
47
+ kind: "string",
48
+ description: "issue priority: high, medium, or low",
49
+ enum: [...SENTRY_ISSUE_PRIORITIES],
50
+ events: [...SENTRY_ISSUE_EVENTS]
51
+ },
52
+ issueCategory: {
53
+ kind: "string",
54
+ description: "kind of problem, such as error, performance, cron, or feedback",
55
+ events: [...SENTRY_ISSUE_EVENTS]
56
+ }
57
+ });
3
58
  function sentryIssueResource(input) {
4
59
  return {
5
60
  identifier: `${input.org}/${input.project}#${input.issueId}`,
@@ -16,6 +71,14 @@ function sentryProjectResource(input) {
16
71
  }
17
72
 
18
73
  // src/events/metric-alert.ts
74
+ import {
75
+ eventMatchFieldsSchema as eventMatchFieldsSchema3
76
+ } from "@sentry/junior-plugin-api";
77
+ var SENTRY_METRIC_ALERT_ACTIONS = [
78
+ "critical",
79
+ "warning",
80
+ "resolved"
81
+ ];
19
82
  var SENTRY_METRIC_ALERT_EVENTS = [
20
83
  "metric_alert.critical",
21
84
  "metric_alert.warning",
@@ -25,6 +88,14 @@ var SENTRY_METRIC_ALERT_SUGGESTED_EVENTS = [
25
88
  "metric_alert.critical",
26
89
  "metric_alert.warning"
27
90
  ];
91
+ var SENTRY_METRIC_ALERT_MATCH_FIELDS = eventMatchFieldsSchema3.parse({
92
+ ...sentryProjectsMatchFields(SENTRY_METRIC_ALERT_EVENTS),
93
+ alertRuleId: {
94
+ kind: "string",
95
+ description: "id of the Sentry metric alert rule, such as 7",
96
+ events: [...SENTRY_METRIC_ALERT_EVENTS]
97
+ }
98
+ });
28
99
  var SENTRY_METRIC_ALERT_IDENTIFIER = {
29
100
  format: "metric-alert:org/alertRuleId",
30
101
  pattern: String.raw`^metric-alert:[^\s/:#]+\/\d+$`
@@ -38,52 +109,55 @@ function sentryMetricAlertResource(input) {
38
109
  }
39
110
 
40
111
  // src/webhooks/events.ts
41
- import { z } from "zod";
42
- var issueWebhookSchema = z.object({
43
- action: z.string(),
44
- data: z.object({
45
- issue: z.object({
46
- culprit: z.string().optional(),
47
- firstSeen: z.string().optional(),
48
- id: z.string().min(1),
49
- issueCategory: z.string().optional(),
50
- issueType: z.string().optional(),
51
- level: z.string().optional(),
52
- priority: z.string().optional(),
53
- project: z.object({ slug: z.string().min(1) }).passthrough(),
54
- status: z.string().optional(),
55
- substatus: z.string().optional().nullable(),
56
- title: z.string().optional(),
57
- url: z.string().url(),
58
- web_url: z.string().url().optional()
112
+ import {
113
+ eventDataSchema
114
+ } from "@sentry/junior-plugin-api";
115
+ import { z as z2 } from "zod";
116
+ var issueLevelSchema = z2.enum(SENTRY_ISSUE_LEVELS).optional().catch(void 0);
117
+ var issuePrioritySchema = z2.enum(SENTRY_ISSUE_PRIORITIES).optional().catch(void 0);
118
+ var issueWebhookSchema = z2.object({
119
+ action: z2.literal("created"),
120
+ data: z2.object({
121
+ issue: z2.object({
122
+ culprit: z2.string().optional(),
123
+ firstSeen: z2.string().optional(),
124
+ id: z2.string().min(1),
125
+ issueCategory: z2.string().optional(),
126
+ issueType: z2.string().optional(),
127
+ level: issueLevelSchema,
128
+ priority: issuePrioritySchema,
129
+ project: z2.object({ slug: z2.string().min(1) }).passthrough(),
130
+ title: z2.string().optional(),
131
+ url: z2.string().url(),
132
+ web_url: z2.string().url().optional()
59
133
  }).passthrough()
60
134
  }),
61
- installation: z.object({ uuid: z.string().min(1) }).passthrough()
135
+ installation: z2.object({ uuid: z2.string().min(1) }).passthrough()
62
136
  }).passthrough();
63
- var metricAlertWebhookSchema = z.object({
64
- action: z.string(),
65
- data: z.object({
66
- description_text: z.string().optional(),
67
- description_title: z.string().optional(),
68
- metric_alert: z.object({
69
- alert_rule: z.object({
70
- id: z.coerce.string().min(1),
71
- projects: z.array(z.string().min(1)).optional()
137
+ var metricAlertWebhookSchema = z2.object({
138
+ action: z2.enum(SENTRY_METRIC_ALERT_ACTIONS),
139
+ data: z2.object({
140
+ description_text: z2.string().optional(),
141
+ description_title: z2.string().optional(),
142
+ metric_alert: z2.object({
143
+ alert_rule: z2.object({
144
+ id: z2.coerce.string().min(1),
145
+ projects: z2.array(z2.string().min(1)).optional()
72
146
  }).passthrough(),
73
- id: z.coerce.string().min(1)
147
+ id: z2.coerce.string().min(1)
74
148
  }).passthrough(),
75
- web_url: z.string().url()
149
+ web_url: z2.string().url()
76
150
  }),
77
- installation: z.object({ uuid: z.string().min(1) }).passthrough()
151
+ installation: z2.object({ uuid: z2.string().min(1) }).passthrough()
78
152
  }).passthrough();
79
- function organizationSlug(issueApiUrl) {
80
- try {
81
- const segments = new URL(issueApiUrl).pathname.split("/").filter(Boolean);
82
- const organizationsIndex = segments.indexOf("organizations");
83
- return organizationsIndex >= 0 ? segments[organizationsIndex + 1] : void 0;
84
- } catch {
85
- return void 0;
86
- }
153
+ function organizationSlug(url) {
154
+ const parsed = z2.url().safeParse(url);
155
+ if (!parsed.success) return void 0;
156
+ const { hostname, pathname } = new URL(parsed.data);
157
+ const segments = pathname.split("/").filter(Boolean);
158
+ const index = segments.indexOf("organizations");
159
+ const slug = index >= 0 ? segments[index + 1] : hostname.split(".").length >= 3 ? hostname.split(".")[0] : void 0;
160
+ return slug?.toLowerCase() || void 0;
87
161
  }
88
162
  function providerTime(value) {
89
163
  if (!value) return void 0;
@@ -97,28 +171,59 @@ function providerTime(value) {
97
171
  function issueText(issue) {
98
172
  const parts = [
99
173
  issue.title ? `Title: ${issue.title}` : void 0,
100
- issue.culprit ? `Culprit: ${issue.culprit}` : void 0,
101
- issue.level ? `Level: ${issue.level}` : void 0,
102
- issue.priority ? `Priority: ${issue.priority}` : void 0,
103
- issue.issueCategory ? `Category: ${issue.issueCategory}` : void 0,
104
- issue.issueType ? `Type: ${issue.issueType}` : void 0,
105
- issue.status ? `Status: ${issue.status}` : void 0,
106
- issue.substatus ? `Substatus: ${issue.substatus}` : void 0,
107
- issue.web_url ? `URL: ${issue.web_url}` : void 0
174
+ issue.culprit ? `Culprit: ${issue.culprit}` : void 0
108
175
  ].filter((part) => part !== void 0);
109
176
  return parts.length > 0 ? parts.join("\n") : void 0;
110
177
  }
111
- function webOrganizationSlug(webUrl) {
112
- const fromPath = organizationSlug(webUrl);
113
- if (fromPath) return fromPath;
114
- const labels = new URL(webUrl).hostname.split(".");
115
- return labels.length >= 3 ? labels[0] : void 0;
178
+ var issueEventDataSchema = z2.object({
179
+ projects: z2.array(sentrySlugDataSchema).length(1),
180
+ issueId: z2.string().min(1),
181
+ level: z2.enum(SENTRY_ISSUE_LEVELS).optional(),
182
+ priority: z2.enum(SENTRY_ISSUE_PRIORITIES).optional(),
183
+ issueCategory: z2.string().min(1).optional(),
184
+ issueType: z2.string().min(1).optional(),
185
+ url: z2.string().url().optional()
186
+ }).strict();
187
+ function issueData(issue) {
188
+ return issueEventDataSchema.parse(
189
+ definedValues({
190
+ projects: [issue.project.slug],
191
+ issueId: issue.id,
192
+ level: issue.level,
193
+ priority: issue.priority,
194
+ issueCategory: issue.issueCategory || void 0,
195
+ issueType: issue.issueType || void 0,
196
+ url: issue.web_url
197
+ })
198
+ );
199
+ }
200
+ var metricAlertEventDataSchema = z2.object({
201
+ projects: z2.array(sentrySlugDataSchema).min(1).optional(),
202
+ alertRuleId: z2.string().min(1),
203
+ incidentId: z2.string().min(1),
204
+ url: z2.string().url()
205
+ }).strict();
206
+ function definedValues(data) {
207
+ return Object.fromEntries(
208
+ Object.entries(data).filter(([, value]) => value !== void 0)
209
+ );
210
+ }
211
+ function metricAlertData(input, projects) {
212
+ const data = metricAlertEventDataSchema.parse(
213
+ definedValues({
214
+ ...input,
215
+ projects: projects.length > 0 ? projects : void 0
216
+ })
217
+ );
218
+ if (eventDataSchema.safeParse(data).success) return data;
219
+ const { projects: _projects, ...rest } = data;
220
+ return rest;
116
221
  }
117
222
  function normalizeIssueEvents(input) {
118
223
  const parsed = issueWebhookSchema.safeParse(input.body);
119
- if (!parsed.success || parsed.data.action !== "created") return [];
224
+ if (!parsed.success) return [];
120
225
  const issue = parsed.data.data.issue;
121
- const org = organizationSlug(issue.url)?.toLowerCase();
226
+ const org = organizationSlug(issue.url);
122
227
  if (!org) return [];
123
228
  const issueResource = sentryIssueResource({
124
229
  issueId: issue.id,
@@ -137,8 +242,10 @@ function normalizeIssueEvents(input) {
137
242
  // idempotency across repeated deliveries of the same lifecycle event.
138
243
  eventKey: `sentry:${issueResource.identifier}:${eventType}`,
139
244
  eventType,
245
+ objectType: "task",
140
246
  occurredAtMs,
141
247
  trustedSummary: `${issueResource.label} was created.`,
248
+ data: issueData(issue),
142
249
  ...untrustedText ? { untrustedText } : void 0
143
250
  };
144
251
  return [
@@ -150,30 +257,33 @@ function normalizeMetricAlertEvents(input) {
150
257
  const parsed = metricAlertWebhookSchema.safeParse(input.body);
151
258
  if (!parsed.success) return [];
152
259
  const eventType = `metric_alert.${parsed.data.action}`;
153
- if (!SENTRY_METRIC_ALERT_EVENTS.includes(eventType)) {
154
- return [];
155
- }
156
260
  const { data } = parsed.data;
157
- const org = webOrganizationSlug(data.web_url)?.toLowerCase();
261
+ const org = organizationSlug(data.web_url);
158
262
  if (!org) return [];
159
263
  const alertRuleId = data.metric_alert.alert_rule.id;
160
264
  const incidentId = data.metric_alert.id;
161
265
  const resource = sentryMetricAlertResource({ alertRuleId, org });
162
- const untrustedText = [
163
- data.description_title,
164
- data.description_text,
165
- `URL: ${data.web_url}`
166
- ].filter((part) => Boolean(part)).join("\n");
266
+ const untrustedText = [data.description_title, data.description_text].filter((part) => Boolean(part)).join("\n");
267
+ const projects = [
268
+ ...new Set(
269
+ (data.metric_alert.alert_rule.projects ?? []).map(
270
+ (project) => project.toLowerCase()
271
+ )
272
+ )
273
+ ];
167
274
  const event = {
168
275
  // One event per incident state. Sentry retries get new Request-IDs.
169
276
  eventKey: `sentry:${resource.identifier}:${incidentId}:${eventType}`,
170
277
  eventType,
278
+ objectType: "item",
171
279
  occurredAtMs: providerTime(input.hookTimestamp) ?? Date.now(),
172
280
  trustedSummary: `${resource.label} is ${parsed.data.action}.`,
173
- data: { alertRuleId, incidentId, url: data.web_url },
174
- untrustedText
281
+ data: metricAlertData(
282
+ { alertRuleId, incidentId, url: data.web_url },
283
+ projects
284
+ ),
285
+ ...untrustedText ? { untrustedText } : void 0
175
286
  };
176
- const projects = [...new Set(data.metric_alert.alert_rule.projects ?? [])];
177
287
  return [
178
288
  { ...event, identifier: resource.identifier },
179
289
  ...projects.map((project) => ({
@@ -253,10 +363,12 @@ function sentryWebhookSecret() {
253
363
 
254
364
  export {
255
365
  SENTRY_ISSUE_EVENTS,
366
+ SENTRY_ISSUE_MATCH_FIELDS,
256
367
  sentryIssueResource,
257
368
  sentryProjectResource,
258
369
  SENTRY_METRIC_ALERT_EVENTS,
259
370
  SENTRY_METRIC_ALERT_SUGGESTED_EVENTS,
371
+ SENTRY_METRIC_ALERT_MATCH_FIELDS,
260
372
  SENTRY_METRIC_ALERT_IDENTIFIER,
261
373
  normalizeSentryEvents,
262
374
  createSentryWebhookRoute,
@@ -1,5 +1,17 @@
1
- import type { SubscribableResource } from "@sentry/junior-plugin-api";
1
+ import { type SubscribableResource } from "@sentry/junior-plugin-api";
2
2
  export declare const SENTRY_ISSUE_EVENTS: readonly ["issue.created"];
3
+ /** Issue levels that Sentry sets, from most to least severe. */
4
+ export declare const SENTRY_ISSUE_LEVELS: readonly ["fatal", "error", "warning", "info", "debug"];
5
+ /** Issue priorities that Sentry sets. */
6
+ export declare const SENTRY_ISSUE_PRIORITIES: readonly ["high", "medium", "low"];
7
+ /** Trusted values that Sentry issue events set, when the webhook has them. */
8
+ export declare const SENTRY_ISSUE_MATCH_FIELDS: Record<string, {
9
+ kind: "boolean" | "number" | "string";
10
+ description: string;
11
+ enum?: string[] | undefined;
12
+ list?: true | undefined;
13
+ events?: string[] | undefined;
14
+ }>;
3
15
  /** Build the stable Sentry issue identity shared by tools and webhooks. */
4
16
  export declare function sentryIssueResource(input: {
5
17
  issueId: string;
@@ -1,7 +1,20 @@
1
- import type { SubscribableResource } from "@sentry/junior-plugin-api";
1
+ import { type SubscribableResource } from "@sentry/junior-plugin-api";
2
+ /** The `action` values of Sentry metric alert webhooks. */
3
+ export declare const SENTRY_METRIC_ALERT_ACTIONS: readonly ["critical", "warning", "resolved"];
2
4
  /** Metric alert states, named after Sentry's `metric_alert.<action>` webhooks. */
3
5
  export declare const SENTRY_METRIC_ALERT_EVENTS: readonly ["metric_alert.critical", "metric_alert.warning", "metric_alert.resolved"];
4
6
  export declare const SENTRY_METRIC_ALERT_SUGGESTED_EVENTS: string[];
7
+ /**
8
+ * Trusted values that Sentry metric alert events set. `projects` is missing
9
+ * when the rule lists no projects or too many to fit in event data.
10
+ */
11
+ export declare const SENTRY_METRIC_ALERT_MATCH_FIELDS: Record<string, {
12
+ kind: "boolean" | "number" | "string";
13
+ description: string;
14
+ enum?: string[] | undefined;
15
+ list?: true | undefined;
16
+ events?: string[] | undefined;
17
+ }>;
5
18
  /** The identifier shape that `sentryMetricAlertResource` builds. */
6
19
  export declare const SENTRY_METRIC_ALERT_IDENTIFIER: {
7
20
  format: string;
@@ -0,0 +1,17 @@
1
+ import { type EventMatchFields } from "@sentry/junior-plugin-api";
2
+ import { z } from "zod";
3
+ /**
4
+ * A project slug in event data. It is lowercase, the same form as
5
+ * identifiers, because the plugin lowercases every identifier.
6
+ */
7
+ export declare const sentrySlugDataSchema: z.ZodString;
8
+ /**
9
+ * Filter on the projects of a Sentry event. `projects` is a list on every
10
+ * event, so one filter fits an issue, which has one project, and a metric
11
+ * alert, whose rule can cover several projects.
12
+ *
13
+ * There is no `org` filter. One Sentry integration belongs to one org, and
14
+ * its webhook secret scopes every delivery to that org. So `org` is the same
15
+ * on every event of an install, and a filter on it cannot narrow anything.
16
+ */
17
+ export declare function sentryProjectsMatchFields(events: readonly string[]): EventMatchFields;
package/dist/index.js CHANGED
@@ -1,15 +1,18 @@
1
1
  import {
2
2
  SENTRY_ISSUE_EVENTS,
3
+ SENTRY_ISSUE_MATCH_FIELDS,
3
4
  SENTRY_METRIC_ALERT_EVENTS,
4
5
  SENTRY_METRIC_ALERT_IDENTIFIER,
6
+ SENTRY_METRIC_ALERT_MATCH_FIELDS,
5
7
  SENTRY_METRIC_ALERT_SUGGESTED_EVENTS,
6
8
  createSentryWebhookRoute,
7
9
  sentryWebhookSecret
8
- } from "./chunk-KK3VWIJ2.js";
10
+ } from "./chunk-EHSVXFHR.js";
9
11
 
10
12
  // src/index.ts
11
13
  import {
12
- defineJuniorPlugin
14
+ defineJuniorPlugin,
15
+ mergeEventMatchFields
13
16
  } from "@sentry/junior-plugin-api";
14
17
 
15
18
  // src/org-read.ts
@@ -101,7 +104,8 @@ function sentryPlugin() {
101
104
  {
102
105
  type: "issue",
103
106
  supportedEvents: [...SENTRY_ISSUE_EVENTS],
104
- suggestedEvents: [...SENTRY_ISSUE_EVENTS]
107
+ suggestedEvents: [...SENTRY_ISSUE_EVENTS],
108
+ matchFields: SENTRY_ISSUE_MATCH_FIELDS
105
109
  },
106
110
  {
107
111
  type: "project",
@@ -112,13 +116,18 @@ function sentryPlugin() {
112
116
  suggestedEvents: [
113
117
  ...SENTRY_ISSUE_EVENTS,
114
118
  ...SENTRY_METRIC_ALERT_SUGGESTED_EVENTS
115
- ]
119
+ ],
120
+ matchFields: mergeEventMatchFields(
121
+ SENTRY_ISSUE_MATCH_FIELDS,
122
+ SENTRY_METRIC_ALERT_MATCH_FIELDS
123
+ )
116
124
  },
117
125
  {
118
126
  type: "metric_alert",
119
127
  identifier: SENTRY_METRIC_ALERT_IDENTIFIER,
120
128
  supportedEvents: [...SENTRY_METRIC_ALERT_EVENTS],
121
- suggestedEvents: SENTRY_METRIC_ALERT_SUGGESTED_EVENTS
129
+ suggestedEvents: SENTRY_METRIC_ALERT_SUGGESTED_EVENTS,
130
+ matchFields: SENTRY_METRIC_ALERT_MATCH_FIELDS
122
131
  }
123
132
  ],
124
133
  isEnabled: () => Boolean(sentryWebhookSecret()),
package/dist/testing.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  sentryIssueResource,
5
5
  sentryProjectResource,
6
6
  sentryWebhookSecret
7
- } from "./chunk-KK3VWIJ2.js";
7
+ } from "./chunk-EHSVXFHR.js";
8
8
  export {
9
9
  createSentryWebhookRoute,
10
10
  normalizeSentryEvents,
@@ -1,4 +1,4 @@
1
- import type { EventInput } from "@sentry/junior-plugin-api";
1
+ import { type EventInput } from "@sentry/junior-plugin-api";
2
2
  interface WebhookInput {
3
3
  body: unknown;
4
4
  hookResource: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-sentry",
3
- "version": "0.265.0",
3
+ "version": "0.266.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "dependencies": {
29
29
  "zod": "^4.5.4",
30
- "@sentry/junior-plugin-api": "0.265.0"
30
+ "@sentry/junior-plugin-api": "0.266.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@oxlint/plugins": "1.79.0",
@@ -35,7 +35,8 @@
35
35
  "oxlint": "^1.79.0",
36
36
  "tsup": "^8.5.1",
37
37
  "typescript": "^7.0.2",
38
- "vitest": "^4.1.12"
38
+ "vitest": "^4.1.12",
39
+ "@sentry/junior-testing": "0.0.0"
39
40
  },
40
41
  "scripts": {
41
42
  "build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",