@sentry/junior-plugin-api 0.266.0 → 0.267.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +121 -11
  2. package/dist/index.js +200 -26
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -1072,6 +1072,10 @@ export declare const eventMatchFieldSchema: z.ZodObject<{
1072
1072
  description: z.ZodString;
1073
1073
  enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
1074
1074
  list: z.ZodOptional<z.ZodLiteral<true>>;
1075
+ case: z.ZodOptional<z.ZodEnum<{
1076
+ lower: "lower";
1077
+ upper: "upper";
1078
+ }>>;
1075
1079
  events: z.ZodOptional<z.ZodArray<z.ZodString>>;
1076
1080
  }, z.core.$strict>;
1077
1081
  export declare const eventMatchFieldsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -1083,6 +1087,10 @@ export declare const eventMatchFieldsSchema: z.ZodRecord<z.ZodString, z.ZodObjec
1083
1087
  description: z.ZodString;
1084
1088
  enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
1085
1089
  list: z.ZodOptional<z.ZodLiteral<true>>;
1090
+ case: z.ZodOptional<z.ZodEnum<{
1091
+ lower: "lower";
1092
+ upper: "upper";
1093
+ }>>;
1086
1094
  events: z.ZodOptional<z.ZodArray<z.ZodString>>;
1087
1095
  }, z.core.$strict>>;
1088
1096
  /** Exact trusted values required before a watch or event automation runs. */
@@ -1098,6 +1106,15 @@ export type EventMatchFields = z.output<typeof eventMatchFieldsSchema>;
1098
1106
  * different values.
1099
1107
  */
1100
1108
  export declare function mergeEventMatchFields(...maps: readonly EventMatchFields[]): EventMatchFields;
1109
+ /**
1110
+ * Merge the match fields of every resource type of one source, for a trigger
1111
+ * with no identifier. Such a trigger can get the events of every resource
1112
+ * type. A field with no `events` list is set on every event of its own
1113
+ * resource type, so it gets that list before the merge. This throws like
1114
+ * `mergeEventMatchFields` when two resource types describe one field
1115
+ * differently.
1116
+ */
1117
+ export declare function anyResourceMatchFields(resourceTypes: readonly Pick<PluginEventType, "matchFields" | "supportedEvents">[]): EventMatchFields;
1101
1118
  /** Stable JSON for one match object. List order does not matter. */
1102
1119
  export declare function stableEventMatchKey(match: EventMatch | undefined): string;
1103
1120
  /**
@@ -1107,6 +1124,12 @@ export declare function stableEventMatchKey(match: EventMatch | undefined): stri
1107
1124
  * match value.
1108
1125
  */
1109
1126
  export declare function eventMatches(match: EventMatch | undefined, data: EventData | undefined): boolean;
1127
+ /**
1128
+ * Return a match with each text value in the case that its field declares,
1129
+ * so a saved filter has the same case as event data. Values of other fields
1130
+ * do not change.
1131
+ */
1132
+ export declare function normalizeEventMatch(fields: EventMatchFields, match: EventMatch | undefined): EventMatch | undefined;
1110
1133
  /** The identifier shape of one resource type. Plain JSON, so API responses can carry it. */
1111
1134
  export declare const eventIdentifierSchema: z.ZodObject<{
1112
1135
  format: z.ZodString;
@@ -1129,10 +1152,38 @@ export declare const pluginEventTypeSchema: z.ZodObject<{
1129
1152
  description: z.ZodString;
1130
1153
  enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
1131
1154
  list: z.ZodOptional<z.ZodLiteral<true>>;
1155
+ case: z.ZodOptional<z.ZodEnum<{
1156
+ lower: "lower";
1157
+ upper: "upper";
1158
+ }>>;
1132
1159
  events: z.ZodOptional<z.ZodArray<z.ZodString>>;
1133
1160
  }, z.core.$strict>>>;
1134
- guidance: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
1135
1161
  }, z.core.$strict>;
1162
+ /**
1163
+ * The event reach of one event type: which event automation triggers can
1164
+ * select it.
1165
+ *
1166
+ * - `"all"`: a trigger can run for every resource of the source, with no
1167
+ * identifier and no filter. Use it only for events that carry status facts
1168
+ * and cause no side effects.
1169
+ * - `{ anyOf }`: a trigger with no identifier must set at least one of these
1170
+ * filters. Use it when an event carries private text, such as titles or
1171
+ * comments, or makes Junior act, such as add a reaction.
1172
+ * - `"resource"`: only a trigger for one resource, with an identifier, can
1173
+ * select the event.
1174
+ */
1175
+ export declare const eventReachSchema: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodLiteral<"resource">, z.ZodObject<{
1176
+ anyOf: z.ZodArray<z.ZodString>;
1177
+ }, z.core.$strict>]>;
1178
+ export type EventReach = z.output<typeof eventReachSchema>;
1179
+ /** What a source declares once for one of its event types. */
1180
+ export declare const pluginEventDeclarationSchema: z.ZodObject<{
1181
+ reach: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodLiteral<"resource">, z.ZodObject<{
1182
+ anyOf: z.ZodArray<z.ZodString>;
1183
+ }, z.core.$strict>]>;
1184
+ guidance: z.ZodOptional<z.ZodString>;
1185
+ }, z.core.$strict>;
1186
+ export type PluginEventDeclaration = z.output<typeof pluginEventDeclarationSchema>;
1136
1187
  export declare const pluginEventsSchema: z.ZodObject<{
1137
1188
  resourceTypes: z.ZodArray<z.ZodObject<{
1138
1189
  type: z.ZodString;
@@ -1151,9 +1202,18 @@ export declare const pluginEventsSchema: z.ZodObject<{
1151
1202
  description: z.ZodString;
1152
1203
  enum: z.ZodOptional<z.ZodArray<z.ZodString>>;
1153
1204
  list: z.ZodOptional<z.ZodLiteral<true>>;
1205
+ case: z.ZodOptional<z.ZodEnum<{
1206
+ lower: "lower";
1207
+ upper: "upper";
1208
+ }>>;
1154
1209
  events: z.ZodOptional<z.ZodArray<z.ZodString>>;
1155
1210
  }, z.core.$strict>>>;
1156
- guidance: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
1211
+ }, z.core.$strict>>;
1212
+ events: z.ZodRecord<z.ZodString, z.ZodObject<{
1213
+ reach: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodLiteral<"resource">, z.ZodObject<{
1214
+ anyOf: z.ZodArray<z.ZodString>;
1215
+ }, z.core.$strict>]>;
1216
+ guidance: z.ZodOptional<z.ZodString>;
1157
1217
  }, z.core.$strict>>;
1158
1218
  isEnabled: z.ZodOptional<z.ZodFunction<z.ZodTuple<readonly [], null>, z.ZodBoolean>>;
1159
1219
  normalizeIdentifier: z.ZodOptional<z.ZodFunction<z.ZodTuple<readonly [z.ZodString], null>, z.ZodString>>;
@@ -1259,10 +1319,17 @@ export interface EventPublisher {
1259
1319
  publish(event: EventInput): Promise<void>;
1260
1320
  /**
1261
1321
  * Return match keys used by active watches or event automations for these
1262
- * identifiers and event types. Plugins use this to load optional trusted
1263
- * data only when a filter needs it.
1322
+ * identifiers and event types, and by event automations with no
1323
+ * identifier. Plugins use this to load optional trusted data only when a
1324
+ * filter needs it.
1325
+ *
1326
+ * Pass `data` with values that every copy of the events sets and that the
1327
+ * plugin already has, such as `repo`. An event automation with no
1328
+ * identifier whose filters on those keys fail adds no keys, so a trigger
1329
+ * for one repository does not load data for events of other repositories.
1264
1330
  */
1265
1331
  neededMatchKeys?(input: {
1332
+ data?: EventData;
1266
1333
  eventTypes: string[];
1267
1334
  identifiers: string[];
1268
1335
  }): Promise<string[]>;
@@ -1273,34 +1340,70 @@ export interface EventTriggerProblem {
1273
1340
  /**
1274
1341
  * - `unknown_resource_type`: the source has no such enabled resource type.
1275
1342
  * - `unknown_event`: the resource type has no such event.
1343
+ * - `identifier_required`: a trigger with no identifier selects an event
1344
+ * that cannot run for every resource.
1345
+ * - `filter_required`: a trigger with no identifier selects an event whose
1346
+ * reach names filters, and the trigger sets none of them.
1276
1347
  * - `unknown_field`: the resource type has no such filter. Saved triggers
1277
1348
  * get this after a plugin renames or removes a filter.
1278
1349
  * - `field_not_set`: a selected event never sets the filter, so the filter
1279
1350
  * never matches that event.
1280
1351
  * - `invalid_value`: the filter value has the wrong type or is not allowed.
1281
1352
  */
1282
- reason: "unknown_resource_type" | "unknown_event" | "unknown_field" | "field_not_set" | "invalid_value";
1353
+ reason: "unknown_resource_type" | "unknown_event" | "identifier_required" | "filter_required" | "unknown_field" | "field_not_set" | "invalid_value";
1283
1354
  /** The filter with the problem. Missing for a resource type or event problem. */
1284
1355
  field?: string;
1285
1356
  /** Plain words for the person or agent who changes the selection. */
1286
1357
  message: string;
1287
1358
  }
1288
- /** The selection to check: one resource type, its events, and its filters. */
1359
+ /**
1360
+ * The selection to check: one resource type, its events, and its filters.
1361
+ * Leave out the resource type for a trigger with no identifier. It runs for
1362
+ * every resource of the source that its filters match.
1363
+ */
1289
1364
  export interface EventTriggerSelection {
1290
- resourceType: string;
1365
+ resourceType?: string;
1291
1366
  events: readonly string[];
1292
1367
  match?: EventMatch;
1293
1368
  }
1369
+ /** The enabled resource types of one source, and the reach of each of its events. */
1370
+ export interface EventTriggerSource {
1371
+ resourceTypes: readonly Pick<PluginEventType, "type" | "matchFields" | "supportedEvents">[];
1372
+ events: Readonly<Record<string, {
1373
+ reach: EventReach;
1374
+ }>>;
1375
+ /**
1376
+ * The filters of every resource type, for a trigger with no identifier.
1377
+ * Merge them once with `anyResourceMatchFields` when the catalog is built,
1378
+ * not for each check.
1379
+ */
1380
+ anyResourceFields: EventMatchFields;
1381
+ }
1382
+ /** The events of a source that a trigger with no identifier can select. */
1383
+ export declare function anyResourceEvents(source: EventTriggerSource): string[];
1294
1384
  /** Return the selected events that never set one filter field. */
1295
1385
  export declare function eventsWithoutMatchField(field: Pick<EventMatchField, "events">, events: readonly string[]): string[];
1386
+ /**
1387
+ * Return the events and filters that a selection can use: those of its
1388
+ * resource type, or with no resource type, the events whose reach is not
1389
+ * `"resource"`, with the filters of every resource type. Return nothing when the source
1390
+ * has no such resource type or cannot run for every resource.
1391
+ */
1392
+ export declare function eventTriggerChoices(source: EventTriggerSource, selection: Pick<EventTriggerSelection, "resourceType">): {
1393
+ events: readonly string[];
1394
+ fields: EventMatchFields;
1395
+ } | undefined;
1296
1396
  /**
1297
1397
  * List why a selection cannot work with the resource types of one source. An
1298
1398
  * empty list means the resource type exists, every selected event is
1299
1399
  * supported, and every selected event can match every filter. A filter on a
1300
1400
  * list field (`list: true`) matches when the event list includes the value.
1401
+ * A selection with no resource type can use only events whose reach is not
1402
+ * `"resource"`, with the filters of every resource type, and must set one of
1403
+ * the filters that the reach of each selected event names.
1301
1404
  * Set `inputKeys` for the agent, so messages name the `match.<key>` input.
1302
1405
  */
1303
- export declare function eventTriggerProblems(resourceTypes: readonly Pick<PluginEventType, "type" | "matchFields" | "supportedEvents">[], selection: EventTriggerSelection, options?: {
1406
+ export declare function eventTriggerProblems(source: EventTriggerSource, selection: EventTriggerSelection, options?: {
1304
1407
  inputKeys?: boolean;
1305
1408
  }): EventTriggerProblem[];
1306
1409
  /** How people see one Event trigger. Each entry is one line of plain text. */
@@ -1321,10 +1424,10 @@ export declare function eventTypeText(eventType: string): string;
1321
1424
  export declare function eventMatchFilterText(key: string, field: EventMatchField | undefined, value: EventMatch[string]): string;
1322
1425
  /**
1323
1426
  * Describe a selection for the dashboard and Slack, so both show the same
1324
- * words. Pass the resource types of the selection's source, as for
1325
- * `eventTriggerProblems`. Its problems are the messages of that check.
1427
+ * words. Pass the selection's source, as for `eventTriggerProblems`. Its
1428
+ * problems are the messages of that check.
1326
1429
  */
1327
- export declare function describeEventTrigger(resourceTypes: readonly Pick<PluginEventType, "type" | "matchFields" | "supportedEvents">[], selection: EventTriggerSelection): EventTriggerDescription;
1430
+ export declare function describeEventTrigger(source: EventTriggerSource, selection: EventTriggerSelection): EventTriggerDescription;
1328
1431
  /** Join filters into one line. Every filter must match, so join with "and". */
1329
1432
  export declare function eventTriggerFiltersText(filters: readonly string[]): string;
1330
1433
  /**
@@ -2065,6 +2168,8 @@ export interface SandboxPrepareHookContext extends PluginContext {
2065
2168
  export interface BeforeToolExecuteHookContext extends PluginContext {
2066
2169
  decision: PluginDecision;
2067
2170
  env: PluginEnv;
2171
+ /** Same directory as `PluginSandbox.juniorRoot` from `sandboxPrepare`. */
2172
+ juniorRoot: string;
2068
2173
  actor?: Actor;
2069
2174
  /** All actors who contributed instructions to the run so far; see `multi-actor-runs.md`. */
2070
2175
  actors?: Actor[];
@@ -2423,6 +2528,11 @@ export interface PluginHooks {
2423
2528
  */
2424
2529
  conversationTitleContext?(ctx: ConversationTitleContextHookContext): Promise<string[] | undefined> | string[] | undefined;
2425
2530
  conversationSidebar?(ctx: ConversationSidebarHookContext): Promise<ConversationSidebarResult> | ConversationSidebarResult;
2531
+ /**
2532
+ * Return static guidance for the system prompt. Return the same text for one
2533
+ * deployment and platform, so the system prompt stays cacheable. Put text
2534
+ * that changes per request in `userPrompt`. See `policies/system-prompt.md`.
2535
+ */
2426
2536
  systemPrompt?(ctx: SystemPromptContext): Promise<PromptMessage[]> | PromptMessage[];
2427
2537
  userPrompt?(ctx: UserPromptContext): Promise<UserPromptContribution[] | undefined> | UserPromptContribution[] | undefined;
2428
2538
  beforeToolExecute?(ctx: BeforeToolExecuteHookContext): Promise<void> | void;
package/dist/index.js CHANGED
@@ -684,6 +684,11 @@ const eventMatchFieldSchema = z.object({
684
684
  */
685
685
  list: z.literal(true).optional(),
686
686
  /**
687
+ * The case of the text in event data. Core changes a saved filter value
688
+ * to this case, so `getsentry/Junior` still matches `getsentry/junior`.
689
+ */
690
+ case: z.enum(["lower", "upper"]).optional(),
691
+ /**
687
692
  * The events that set this field. Leave it out when every event of the
688
693
  * resource type sets it. A filter on this field never matches other
689
694
  * events, so a trigger that selects them is rejected.
@@ -695,6 +700,11 @@ const eventMatchFieldSchema = z.object({
695
700
  message: "Event match field enum requires kind \"string\".",
696
701
  path: ["enum"]
697
702
  });
703
+ if (field.case && field.kind !== "string") context.addIssue({
704
+ code: "custom",
705
+ message: "Event match field case requires kind \"string\".",
706
+ path: ["case"]
707
+ });
698
708
  if (field.list && field.kind === "boolean") context.addIssue({
699
709
  code: "custom",
700
710
  message: "Event match field list cannot use kind \"boolean\".",
@@ -746,7 +756,7 @@ function mergeEventMatchFields(...maps) {
746
756
  merged[key] = field;
747
757
  continue;
748
758
  }
749
- if (current.kind !== field.kind || current.list !== field.list || !sameValues(current.enum, field.enum)) throw new Error(`Match field "${key}" has a different kind, enum, or list in two maps.`);
759
+ if (current.kind !== field.kind || current.list !== field.list || current.case !== field.case || !sameValues(current.enum, field.enum)) throw new Error(`Match field "${key}" has a different kind, case, enum, or list in two maps.`);
750
760
  if (!current.events || !field.events) throw new Error(`Match field "${key}" is in two maps, so each copy must list its events.`);
751
761
  merged[key] = {
752
762
  ...current,
@@ -755,6 +765,20 @@ function mergeEventMatchFields(...maps) {
755
765
  }
756
766
  return eventMatchFieldsSchema.parse(merged);
757
767
  }
768
+ /**
769
+ * Merge the match fields of every resource type of one source, for a trigger
770
+ * with no identifier. Such a trigger can get the events of every resource
771
+ * type. A field with no `events` list is set on every event of its own
772
+ * resource type, so it gets that list before the merge. This throws like
773
+ * `mergeEventMatchFields` when two resource types describe one field
774
+ * differently.
775
+ */
776
+ function anyResourceMatchFields(resourceTypes) {
777
+ return mergeEventMatchFields(...resourceTypes.map((resourceType) => Object.fromEntries(Object.entries(resourceType.matchFields ?? {}).map(([key, field]) => [key, field.events ? field : {
778
+ ...field,
779
+ events: [...resourceType.supportedEvents]
780
+ }]))));
781
+ }
758
782
  function stableMatchValue(value) {
759
783
  if (!Array.isArray(value)) return value;
760
784
  return [...value].sort((left, right) => {
@@ -784,6 +808,20 @@ function eventMatches(match, data) {
784
808
  }
785
809
  return true;
786
810
  }
811
+ /**
812
+ * Return a match with each text value in the case that its field declares,
813
+ * so a saved filter has the same case as event data. Values of other fields
814
+ * do not change.
815
+ */
816
+ function normalizeEventMatch(fields, match) {
817
+ if (!match) return void 0;
818
+ const toCase = (key, value) => {
819
+ const fieldCase = fields[key]?.case;
820
+ if (typeof value !== "string" || !fieldCase) return value;
821
+ return fieldCase === "lower" ? value.toLowerCase() : value.toUpperCase();
822
+ };
823
+ return Object.fromEntries(Object.entries(match).map(([key, value]) => [key, Array.isArray(value) ? value.map((entry) => toCase(key, entry)) : typeof value === "boolean" ? value : toCase(key, value)]));
824
+ }
787
825
  function isRegExpSource(value) {
788
826
  try {
789
827
  new RegExp(value);
@@ -804,8 +842,7 @@ const pluginEventTypeSchema = z.object({
804
842
  identifier: eventIdentifierSchema.optional(),
805
843
  supportedEvents: z.array(eventTypeSchema).min(1),
806
844
  suggestedEvents: z.array(eventTypeSchema).optional(),
807
- matchFields: eventMatchFieldsSchema.optional(),
808
- guidance: z.record(eventTypeSchema, z.string().trim().min(1).max(EVENT_GUIDANCE_MAX_LENGTH)).optional()
845
+ matchFields: eventMatchFieldsSchema.optional()
809
846
  }).strict().superRefine((resourceType, context) => {
810
847
  const supported = /* @__PURE__ */ new Set();
811
848
  resourceType.supportedEvents.forEach((eventType, index) => {
@@ -816,11 +853,6 @@ const pluginEventTypeSchema = z.object({
816
853
  });
817
854
  supported.add(eventType);
818
855
  });
819
- for (const eventType of Object.keys(resourceType.guidance ?? {})) if (!supported.has(eventType)) context.addIssue({
820
- code: "custom",
821
- message: `Guidance event type "${eventType}" is not supported.`,
822
- path: ["guidance", eventType]
823
- });
824
856
  const suggested = /* @__PURE__ */ new Set();
825
857
  resourceType.suggestedEvents?.forEach((eventType, index) => {
826
858
  if (suggested.has(eventType)) context.addIssue({
@@ -848,8 +880,41 @@ const pluginEventTypeSchema = z.object({
848
880
  });
849
881
  });
850
882
  });
883
+ /**
884
+ * The event reach of one event type: which event automation triggers can
885
+ * select it.
886
+ *
887
+ * - `"all"`: a trigger can run for every resource of the source, with no
888
+ * identifier and no filter. Use it only for events that carry status facts
889
+ * and cause no side effects.
890
+ * - `{ anyOf }`: a trigger with no identifier must set at least one of these
891
+ * filters. Use it when an event carries private text, such as titles or
892
+ * comments, or makes Junior act, such as add a reaction.
893
+ * - `"resource"`: only a trigger for one resource, with an identifier, can
894
+ * select the event.
895
+ */
896
+ const eventReachSchema = z.union([
897
+ z.literal("all"),
898
+ z.literal("resource"),
899
+ z.object({ anyOf: z.array(z.string().regex(EVENT_MATCH_FIELD_NAME)).min(1) }).strict()
900
+ ]);
901
+ /** What a source declares once for one of its event types. */
902
+ const pluginEventDeclarationSchema = z.object({
903
+ reach: eventReachSchema,
904
+ /**
905
+ * App guidance that the agent gets with each Event of this type, for every
906
+ * Watch and event automation.
907
+ */
908
+ guidance: z.string().trim().min(1).max(EVENT_GUIDANCE_MAX_LENGTH).optional()
909
+ }).strict();
851
910
  const pluginEventsSchema = z.object({
852
911
  resourceTypes: z.array(pluginEventTypeSchema).min(1),
912
+ /**
913
+ * One declaration for each event type that a resource type supports. An
914
+ * event that two resource types share has one declaration, so its reach
915
+ * and guidance are the same for both.
916
+ */
917
+ events: z.record(eventTypeSchema, pluginEventDeclarationSchema),
853
918
  isEnabled: z.function({
854
919
  input: [],
855
920
  output: z.boolean()
@@ -872,6 +937,41 @@ const pluginEventsSchema = z.object({
872
937
  });
873
938
  seen.add(resourceType.type);
874
939
  });
940
+ const supported = new Set(registration.resourceTypes.flatMap((resourceType) => resourceType.supportedEvents));
941
+ for (const eventType of supported) if (!registration.events[eventType]) context.addIssue({
942
+ code: "custom",
943
+ message: `Event type "${eventType}" has no declaration in events.`,
944
+ path: ["events"]
945
+ });
946
+ let fields;
947
+ try {
948
+ fields = anyResourceMatchFields(registration.resourceTypes);
949
+ } catch (error) {
950
+ context.addIssue({
951
+ code: "custom",
952
+ message: error.message,
953
+ path: ["resourceTypes"]
954
+ });
955
+ return;
956
+ }
957
+ for (const [eventType, declaration] of Object.entries(registration.events)) {
958
+ if (!supported.has(eventType)) context.addIssue({
959
+ code: "custom",
960
+ message: `Declared event type "${eventType}" is not supported.`,
961
+ path: ["events", eventType]
962
+ });
963
+ if (typeof declaration.reach === "string") continue;
964
+ for (const filter of declaration.reach.anyOf) if (!fields[filter]?.events?.includes(eventType)) context.addIssue({
965
+ code: "custom",
966
+ message: `Reach filter "${filter}" is not set on "${eventType}".`,
967
+ path: [
968
+ "events",
969
+ eventType,
970
+ "reach",
971
+ "anyOf"
972
+ ]
973
+ });
974
+ }
875
975
  });
876
976
  /** Apply a plugin's identifier convention at event boundaries. */
877
977
  function normalizeEventIdentifier(registration, identifier) {
@@ -928,6 +1028,10 @@ const eventInputSchema = z.object({
928
1028
  }).strict();
929
1029
  const eventSchema = eventInputSchema.extend({ namespace: z.string().min(1) });
930
1030
  // src/event-triggers.ts
1031
+ /** The events of a source that a trigger with no identifier can select. */
1032
+ function anyResourceEvents(source) {
1033
+ return Object.entries(source.events).filter(([, declaration]) => declaration.reach !== "resource").map(([eventType]) => eventType).sort();
1034
+ }
931
1035
  function quoted(values) {
932
1036
  return values.map((value) => `"${value}"`).join(", ");
933
1037
  }
@@ -950,38 +1054,85 @@ function valueProblem(filter, field, value) {
950
1054
  const notAllowed = allowed ? values.find((entry) => !allowed.includes(String(entry))) : void 0;
951
1055
  if (allowed && notAllowed !== void 0) return `${filter} cannot be "${notAllowed}". Use one of: ${quoted(allowed)}.`;
952
1056
  }
1057
+ /** The events and filters that one selection can use. */
1058
+ function selectionScope(source, selection) {
1059
+ if (selection.resourceType === void 0) {
1060
+ const events = anyResourceEvents(source);
1061
+ if (events.length === 0) return {
1062
+ reason: "identifier_required",
1063
+ message: "This source cannot run for every resource. Set a resource type and an identifier."
1064
+ };
1065
+ return {
1066
+ events,
1067
+ fields: source.anyResourceFields,
1068
+ owner: "source"
1069
+ };
1070
+ }
1071
+ const resourceType = source.resourceTypes.find((candidate) => candidate.type === selection.resourceType);
1072
+ if (!resourceType) return {
1073
+ reason: "unknown_resource_type",
1074
+ message: `"${selection.resourceType}" is not an enabled resource type. Use one of: ${quoted(source.resourceTypes.map((candidate) => candidate.type))}.`
1075
+ };
1076
+ return {
1077
+ events: resourceType.supportedEvents,
1078
+ fields: resourceType.matchFields ?? {},
1079
+ owner: "resource type"
1080
+ };
1081
+ }
1082
+ /**
1083
+ * Return the events and filters that a selection can use: those of its
1084
+ * resource type, or with no resource type, the events whose reach is not
1085
+ * `"resource"`, with the filters of every resource type. Return nothing when the source
1086
+ * has no such resource type or cannot run for every resource.
1087
+ */
1088
+ function eventTriggerChoices(source, selection) {
1089
+ if (source.resourceTypes.length === 0) return void 0;
1090
+ const scope = selectionScope(source, selection);
1091
+ return "reason" in scope ? void 0 : {
1092
+ events: scope.events,
1093
+ fields: scope.fields
1094
+ };
1095
+ }
953
1096
  /**
954
1097
  * List why a selection cannot work with the resource types of one source. An
955
1098
  * empty list means the resource type exists, every selected event is
956
1099
  * supported, and every selected event can match every filter. A filter on a
957
1100
  * list field (`list: true`) matches when the event list includes the value.
1101
+ * A selection with no resource type can use only events whose reach is not
1102
+ * `"resource"`, with the filters of every resource type, and must set one of
1103
+ * the filters that the reach of each selected event names.
958
1104
  * Set `inputKeys` for the agent, so messages name the `match.<key>` input.
959
1105
  */
960
- function eventTriggerProblems(resourceTypes, selection, options = {}) {
961
- const resourceType = resourceTypes.find((candidate) => candidate.type === selection.resourceType);
962
- if (!resourceType) return [{
1106
+ function eventTriggerProblems(source, selection, options = {}) {
1107
+ if (source.resourceTypes.length === 0) return [{
963
1108
  reason: "unknown_resource_type",
964
- message: resourceTypes.length === 0 ? "The plugin for this source is not enabled." : `"${selection.resourceType}" is not an enabled resource type. Use one of: ${quoted(resourceTypes.map((candidate) => candidate.type))}.`
1109
+ message: "The plugin for this source is not enabled."
965
1110
  }];
1111
+ const scope = selectionScope(source, selection);
1112
+ if ("reason" in scope) return [scope];
1113
+ const filterName = (key) => options.inputKeys ? `Filter "${key}" (match.${key})` : `Filter "${key}"`;
966
1114
  const problems = [];
967
- const events = selection.events.filter((eventType) => resourceType.supportedEvents.includes(eventType));
1115
+ const events = selection.events.filter((eventType) => scope.events.includes(eventType));
968
1116
  for (const eventType of selection.events) {
969
1117
  if (events.includes(eventType)) continue;
970
- problems.push({
1118
+ const anySupports = source.resourceTypes.some((resourceType) => resourceType.supportedEvents.includes(eventType));
1119
+ problems.push(selection.resourceType === void 0 && anySupports ? {
1120
+ reason: "identifier_required",
1121
+ message: `"${eventType}" cannot run for every resource. Set a resource type and an identifier, or use one of: ${quoted(scope.events)}.`
1122
+ } : {
971
1123
  reason: "unknown_event",
972
- message: `"${eventType}" is not an event for this resource type. Use one of: ${quoted(resourceType.supportedEvents)}.`
1124
+ message: `"${eventType}" is not an event for this ${scope.owner}. Use one of: ${quoted(scope.events)}.`
973
1125
  });
974
1126
  }
975
- const fields = resourceType.matchFields ?? {};
976
- const fieldNames = Object.keys(fields);
1127
+ const fieldNames = Object.keys(scope.fields);
977
1128
  for (const [key, value] of Object.entries(selection.match ?? {})) {
978
- const filter = options.inputKeys ? `Filter "${key}" (match.${key})` : `Filter "${key}"`;
979
- const field = fields[key];
1129
+ const filter = filterName(key);
1130
+ const field = scope.fields[key];
980
1131
  if (!field) {
981
1132
  problems.push({
982
1133
  reason: "unknown_field",
983
1134
  field: key,
984
- message: fieldNames.length === 0 ? `${filter} does not exist. This resource type has no filters, so remove it.` : `${filter} does not exist for this resource type. Remove it, or use one of: ${quoted(fieldNames)}.`
1135
+ message: fieldNames.length === 0 ? `${filter} does not exist. This ${scope.owner} has no filters, so remove it.` : `${filter} does not exist for this ${scope.owner}. Remove it, or use one of: ${quoted(fieldNames)}.`
985
1136
  });
986
1137
  continue;
987
1138
  }
@@ -998,8 +1149,31 @@ function eventTriggerProblems(resourceTypes, selection, options = {}) {
998
1149
  message: invalid
999
1150
  });
1000
1151
  }
1152
+ if (selection.resourceType === void 0) problems.push(...requiredFilterProblems(source, events, selection, options));
1001
1153
  return problems;
1002
1154
  }
1155
+ /** Name the filters that the selected events need and the trigger does not set. */
1156
+ function requiredFilterProblems(source, events, selection, options) {
1157
+ const missing = /* @__PURE__ */ new Map();
1158
+ for (const eventType of events) {
1159
+ const reach = source.events[eventType]?.reach;
1160
+ if (typeof reach !== "object") continue;
1161
+ if (reach.anyOf.some((filter) => selection.match?.[filter] !== void 0)) continue;
1162
+ const key = [...reach.anyOf].sort().join(",");
1163
+ const entry = missing.get(key) ?? {
1164
+ anyOf: reach.anyOf,
1165
+ events: []
1166
+ };
1167
+ entry.events.push(eventType);
1168
+ missing.set(key, entry);
1169
+ }
1170
+ const inputKey = (filter) => options.inputKeys ? ` (match.${filter})` : "";
1171
+ return [...missing.values()].map(({ anyOf, events: requiredBy }) => ({
1172
+ reason: "filter_required",
1173
+ field: anyOf[0],
1174
+ message: anyOf.length === 1 ? `This trigger needs a "${anyOf[0]}" filter${inputKey(anyOf[0])}, because ${quoted(requiredBy)} cannot run for every resource.` : `This trigger needs one of these filters: ${anyOf.map((filter) => `"${filter}"${inputKey(filter)}`).join(", ")}, because ${quoted(requiredBy)} cannot run for every resource.`
1175
+ }));
1176
+ }
1003
1177
  /** Write one event type in plain words: `pull_request.opened` is "pull request opened". */
1004
1178
  function eventTypeText(eventType) {
1005
1179
  return eventType.replaceAll(/[._]/g, " ");
@@ -1014,15 +1188,15 @@ function eventMatchFilterText(key, field, value) {
1014
1188
  }
1015
1189
  /**
1016
1190
  * Describe a selection for the dashboard and Slack, so both show the same
1017
- * words. Pass the resource types of the selection's source, as for
1018
- * `eventTriggerProblems`. Its problems are the messages of that check.
1191
+ * words. Pass the selection's source, as for `eventTriggerProblems`. Its
1192
+ * problems are the messages of that check.
1019
1193
  */
1020
- function describeEventTrigger(resourceTypes, selection) {
1021
- const fields = resourceTypes.find((candidate) => candidate.type === selection.resourceType)?.matchFields ?? {};
1194
+ function describeEventTrigger(source, selection) {
1195
+ const fields = eventTriggerChoices(source, selection)?.fields ?? {};
1022
1196
  return {
1023
1197
  events: selection.events.map(eventTypeText),
1024
1198
  filters: Object.entries(selection.match ?? {}).map(([key, value]) => eventMatchFilterText(key, fields[key], value)),
1025
- problems: eventTriggerProblems(resourceTypes, selection).map((problem) => problem.message)
1199
+ problems: eventTriggerProblems(source, selection).map((problem) => problem.message)
1026
1200
  };
1027
1201
  }
1028
1202
  /** Join filters into one line. Every filter must match, so join with "and". */
@@ -1552,4 +1726,4 @@ const pluginUserPageInputSchema = z.object({
1552
1726
  limit: z.number().int().min(1).max(50),
1553
1727
  query: pluginUserPageQuerySchema
1554
1728
  }).strict();
1555
- export { EVENT_DATA_MAX_JSON_BYTES, EVENT_DATA_MAX_KEYS, EVENT_GUIDANCE_MAX_LENGTH, EVENT_SUMMARY_MAX_LENGTH, EVENT_TEXT_MAX_LENGTH, EgressAuthRequired, EgressPolicyDenied, PluginToolInputError, REQUIRED_TOOL_ANNOTATION_KEYS, actorSchema, actorUserIdSchema, agentInvocationSourceSchema, codeChangeInputSchema, codeChangeStateSchema, conversationAnnotationInputSchema, conversationEventIconSchema, conversationEventPresentationSchema, conversationSidebarAnnotationSchema, conversationSidebarIconSchema, createEventSource, createLocalSource, createSlackSource, createWebSource, defineConversationEvent, defineJuniorPlugin, definePluginTool, definePromptContext, describeEventTrigger, destinationSchema, destinationVisibilitySchema, dispatchOptionsSchema, enforceEgressPolicy, eventAutomationSourceSchema, eventDataSchema, eventIdentifierSchema, eventInputSchema, eventMatchFieldSchema, eventMatchFieldsSchema, eventMatchFilterText, eventMatchSchema, eventMatches, eventPersonSchema, eventSchema, eventSourceSchema, eventTriggerFiltersText, eventTriggerProblems, eventTriggerText, eventTypeSchema, eventTypeText, eventsWithoutMatchField, getSourceKey, identitySchema, isPrivateSource, isSlackDestination, localActorSchema, localDestinationSchema, localSourceSchema, locationSchema, mergeEventMatchFields, missingToolAnnotationKeys, nonBlankStringSchema, normalizeEventIdentifier, objectAnnotationSchema, objectFactFields, objectFactsSchema, objectIconPath, objectIconPaths, objectPresentation, objectTypeSchema, ownedObjectAnnotationSchema, parseShellCommands, platformSchema, pluginApiRouteRequestContextSchema, pluginAuthorizationSchema, pluginCredentialHeaderTransformSchema, pluginCredentialLeaseSchema, pluginCredentialResultSchema, pluginCredentialSubjectSchema, pluginDispatchSourceSchema, pluginEventTypeSchema, pluginEventsSchema, pluginGrantSchema, pluginProviderAccountSchema, pluginRunContextSchema, pluginRunTranscriptEntrySchema, pluginRunTranscriptProvenanceSchema, pluginStoredTokensSchema, pluginToolContentSchema, pluginToolContinuationSchema, pluginToolOutputSchema, pluginUserPageContentSchema, pluginUserPageCursorSchema, pluginUserPageFilterSchema, pluginUserPageInputSchema, pluginUserPageLinkSchema, pluginUserPageLinksSchema, pluginUserPageQuerySchema, promptContextSchema, promptMessageSchema, replyAttributionSchema, resourceLinkAnnotationSchema, resourceTypeSchema, scheduledAutomationSourceSchema, slackActorSchema, slackDestinationSchema, slackLocationSchema, slackSourceSchema, sourceSchema, sourceVisibilitySchema, stableEventMatchKey, subscribableResourceSchema, systemActorSchema, taskOutcomeSchema, toolApprovalModeSchema, userSchema, watchResultSchema, webActorSchema, webSourceSchema, zodTool };
1729
+ export { EVENT_DATA_MAX_JSON_BYTES, EVENT_DATA_MAX_KEYS, EVENT_GUIDANCE_MAX_LENGTH, EVENT_SUMMARY_MAX_LENGTH, EVENT_TEXT_MAX_LENGTH, EgressAuthRequired, EgressPolicyDenied, PluginToolInputError, REQUIRED_TOOL_ANNOTATION_KEYS, actorSchema, actorUserIdSchema, agentInvocationSourceSchema, anyResourceEvents, anyResourceMatchFields, codeChangeInputSchema, codeChangeStateSchema, conversationAnnotationInputSchema, conversationEventIconSchema, conversationEventPresentationSchema, conversationSidebarAnnotationSchema, conversationSidebarIconSchema, createEventSource, createLocalSource, createSlackSource, createWebSource, defineConversationEvent, defineJuniorPlugin, definePluginTool, definePromptContext, describeEventTrigger, destinationSchema, destinationVisibilitySchema, dispatchOptionsSchema, enforceEgressPolicy, eventAutomationSourceSchema, eventDataSchema, eventIdentifierSchema, eventInputSchema, eventMatchFieldSchema, eventMatchFieldsSchema, eventMatchFilterText, eventMatchSchema, eventMatches, eventPersonSchema, eventReachSchema, eventSchema, eventSourceSchema, eventTriggerChoices, eventTriggerFiltersText, eventTriggerProblems, eventTriggerText, eventTypeSchema, eventTypeText, eventsWithoutMatchField, getSourceKey, identitySchema, isPrivateSource, isSlackDestination, localActorSchema, localDestinationSchema, localSourceSchema, locationSchema, mergeEventMatchFields, missingToolAnnotationKeys, nonBlankStringSchema, normalizeEventIdentifier, normalizeEventMatch, objectAnnotationSchema, objectFactFields, objectFactsSchema, objectIconPath, objectIconPaths, objectPresentation, objectTypeSchema, ownedObjectAnnotationSchema, parseShellCommands, platformSchema, pluginApiRouteRequestContextSchema, pluginAuthorizationSchema, pluginCredentialHeaderTransformSchema, pluginCredentialLeaseSchema, pluginCredentialResultSchema, pluginCredentialSubjectSchema, pluginDispatchSourceSchema, pluginEventDeclarationSchema, pluginEventTypeSchema, pluginEventsSchema, pluginGrantSchema, pluginProviderAccountSchema, pluginRunContextSchema, pluginRunTranscriptEntrySchema, pluginRunTranscriptProvenanceSchema, pluginStoredTokensSchema, pluginToolContentSchema, pluginToolContinuationSchema, pluginToolOutputSchema, pluginUserPageContentSchema, pluginUserPageCursorSchema, pluginUserPageFilterSchema, pluginUserPageInputSchema, pluginUserPageLinkSchema, pluginUserPageLinksSchema, pluginUserPageQuerySchema, promptContextSchema, promptMessageSchema, replyAttributionSchema, resourceLinkAnnotationSchema, resourceTypeSchema, scheduledAutomationSourceSchema, slackActorSchema, slackDestinationSchema, slackLocationSchema, slackSourceSchema, sourceSchema, sourceVisibilitySchema, stableEventMatchKey, subscribableResourceSchema, systemActorSchema, taskOutcomeSchema, toolApprovalModeSchema, userSchema, watchResultSchema, webActorSchema, webSourceSchema, zodTool };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-plugin-api",
3
- "version": "0.266.0",
3
+ "version": "0.267.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"