@plainconceptsplatform/workflows 0.28.6 → 0.28.7

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.
@@ -32,6 +32,9 @@ export const FINDING_FIELDS = [
32
32
  "firstSeen",
33
33
  "lastSeen",
34
34
  "frames",
35
+ "topFrames",
36
+ "message",
37
+ "category",
35
38
  "fingerprint",
36
39
  ];
37
40
 
@@ -112,6 +115,16 @@ export function ownFrames(details, prefix) {
112
115
  .slice(0, MAX_FRAMES);
113
116
  }
114
117
 
118
+ export function topFrames(details) {
119
+ const stacks = (Array.isArray(details) ? details : []).flatMap((detail) =>
120
+ Array.isArray(detail?.parsedStack) ? detail.parsedStack : []);
121
+
122
+ return stacks
123
+ .filter((frame) => typeof frame?.method === "string")
124
+ .map((frame) => clip(frame.method))
125
+ .slice(0, MAX_FRAMES);
126
+ }
127
+
115
128
  /** Masks everything in SCRUBS. Applied to every string before it is rendered or scanned. */
116
129
  export function scrub(value) {
117
130
  return SCRUBS.reduce((text, [pattern, replacement]) => text.replace(pattern, replacement), value);
@@ -162,6 +175,9 @@ export function toFindings(rows, { environment, ownCodePrefix = "", minOccurrenc
162
175
  firstSeen: clip(row.FirstSeen),
163
176
  lastSeen: clip(row.LastSeen),
164
177
  frames: ownFrames(row.AnyDetails, ownCodePrefix),
178
+ topFrames: topFrames(row.AnyDetails),
179
+ message: clip(row.AnyMessage),
180
+ category: clip(row.AnyCategory),
165
181
  fingerprint: fingerprint(clip(row.ProblemId), clip(row.OperationName), environment),
166
182
  }))
167
183
  .filter((finding) => finding.occurrences >= minOccurrences && finding.operations >= minOperations)
@@ -177,34 +193,41 @@ export function toFindings(rows, { environment, ownCodePrefix = "", minOccurrenc
177
193
  */
178
194
  export function render(finding, { environment, lookbackHours, ownCodePrefix = "" }) {
179
195
  const marker = `<!-- pcp-app-error: ${finding.fingerprint} -->`;
180
- const title = `${finding.exceptionType} in ${finding.operationName || finding.roleName || environment}`;
196
+ const title = `${finding.exceptionType} in ${finding.operationName || finding.category || finding.roleName || environment}`;
181
197
 
182
198
  const body = [
183
199
  marker,
184
200
  `**${finding.occurrences} occurrence(s)** in the last ${lookbackHours}h in \`${environment}\`, across ${finding.operations} operation(s).`,
185
201
  "",
186
202
  `- Exception: \`${finding.exceptionType}\``,
203
+ finding.message ? `- Message: \`${finding.message}\`` : null,
187
204
  `- Operation: \`${finding.operationName || "(none recorded)"}\``,
205
+ finding.category ? `- Source: \`${finding.category}\`` : null,
188
206
  `- Role: \`${finding.roleName || "(none recorded)"}\``,
189
207
  `- First seen: ${finding.firstSeen}`,
190
208
  `- Last seen: ${finding.lastSeen}`,
191
209
  finding.frames.length > 0
192
210
  ? `\n**Our own frames, outermost first**\n\n\`\`\`\n${finding.frames.join("\n")}\n\`\`\``
193
- : ownCodePrefix
194
- // The prefix is set and nothing matched: every frame belongs to the framework or to a
195
- // dependency. Routine for an exception raised inside framework code -- a cancelled task
196
- // is the common one -- and nothing for the reader to go and configure.
197
- ? `\n_No frame in this exception belongs to \`${clip(ownCodePrefix)}\`: all of them are the framework's or a dependency's. That is usual for an exception thrown inside framework code, such as a cancelled task._`
198
- // No prefix, so no frame can be recognised as this repository's own and the
199
- // stack is dropped rather than publishing somebody else's file paths.
200
- : "\n_No stack frames are shown: this repository has not said which assemblies are its own. Set `OWN_CODE_PREFIX` in the workflow to name them._",
211
+ : finding.topFrames.length > 0
212
+ ? ownCodePrefix
213
+ // The prefix is set and nothing matched: every frame belongs to the framework or to a
214
+ // dependency. Show the top frames anyway, because a framework stack is still better
215
+ // than nothing when the exception is something like a cancelled task raised inside
216
+ // framework code.
217
+ ? `\n_No frame in this exception belongs to \`${clip(ownCodePrefix)}\`: all of them are the framework's or a dependency's._\n\n**Top stack frames, outermost first**\n\n\`\`\`\n${finding.topFrames.join("\n")}\n\`\`\``
218
+ : `\n**Top stack frames, outermost first**\n\n\`\`\`\n${finding.topFrames.join("\n")}\n\`\`\``
219
+ : ownCodePrefix
220
+ ? `\n_No frame in this exception belongs to \`${clip(ownCodePrefix)}\`: all of them are the framework's or a dependency's. That is usual for an exception thrown inside framework code, such as a cancelled task._`
221
+ // No prefix, so no frame can be recognised as this repository's own and the
222
+ // stack is dropped rather than publishing somebody else's file paths.
223
+ : "\n_No stack frames are shown: this repository has not said which assemblies are its own. Set `OWN_CODE_PREFIX` in the workflow to name them._",
201
224
  "",
202
225
  "---",
203
226
  "",
204
227
  "Filed automatically from Application Insights. Nothing here was read by a model, and no",
205
228
  "identifier that joins back to a person is included, so the reproduction is still somebody's",
206
229
  "to work out. Close this with the `error-accepted` label to stop it being filed again.",
207
- ].join("\n");
230
+ ].filter(Boolean).join("\n");
208
231
 
209
232
  return { title, body, marker };
210
233
  }
@@ -47,20 +47,25 @@ let window = ago(${LOOKBACK_HOURS}h);
47
47
  let thrown =
48
48
  AppExceptions
49
49
  | where TimeGenerated > window
50
- | extend Problem = ProblemId, Kind = ExceptionType, Stack = Details;
50
+ | extend Problem = ProblemId, Kind = ExceptionType, Stack = Details,
51
+ Msg = outerMessage, Category = "";
51
52
  let logged =
52
53
  AppTraces
53
54
  | where TimeGenerated > window
54
55
  | where SeverityLevel >= 3
55
56
  | extend Problem = tostring(coalesce(Properties["OriginalFormat"], Message)),
56
57
  Kind = tostring(coalesce(Properties["ExceptionType"], "LoggedError")),
57
- Stack = dynamic([]);
58
+ Stack = dynamic([]),
59
+ Msg = Message,
60
+ Category = tostring(coalesce(Properties["SourceContext"], Properties["Category"], ""));
58
61
  union thrown, logged
59
62
  | summarize Occurrences = sum(ItemCount),
60
63
  Operations = dcount(OperationName),
61
64
  FirstSeen = min(TimeGenerated),
62
65
  LastSeen = max(TimeGenerated),
63
- AnyDetails = any(Stack)
66
+ AnyDetails = any(Stack),
67
+ AnyMessage = any(Msg),
68
+ AnyCategory = any(Category)
64
69
  by ProblemId = Problem, ExceptionType = Kind, OperationName, AppRoleName
65
70
  | where Occurrences >= ${MIN_OCCURRENCES}
66
71
  | order by Occurrences desc
@@ -14,6 +14,7 @@ import {
14
14
  fingerprint,
15
15
  leaks,
16
16
  ownFrames,
17
+ topFrames,
17
18
  prepare,
18
19
  render,
19
20
  scrub,
@@ -49,6 +50,8 @@ function row(overrides = {}) {
49
50
  Operations: 3,
50
51
  FirstSeen: "2026-09-13T01:10:00Z",
51
52
  LastSeen: "2026-09-13T06:40:00Z",
53
+ AnyMessage: "Sequence contains no elements",
54
+ AnyCategory: "Pliny.Application.Agents.RunExecutor",
52
55
  AnyOperationId: "6f1c1b0e9a2f4d55",
53
56
  AnyDetails: [
54
57
  {
@@ -113,6 +116,16 @@ check(
113
116
 
114
117
  check("a row with no details does not throw", ownFrames(undefined, "Pliny").length === 0);
115
118
 
119
+ check(
120
+ "topFrames returns frames without prefix filter",
121
+ topFrames(row().AnyDetails).length >= 1);
122
+
123
+ check(
124
+ "topFrames includes framework methods",
125
+ topFrames(row().AnyDetails).includes("Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke"));
126
+
127
+ check("topFrames empty when no details", topFrames(undefined).length === 0);
128
+
116
129
  section("Fingerprints");
117
130
 
118
131
  check(
@@ -158,6 +171,16 @@ check(
158
171
  "run_id is not a reportable field",
159
172
  !FINDING_FIELDS.includes("runId") && !FINDING_FIELDS.includes("run_id"));
160
173
 
174
+ check(
175
+ "new fields are in the allowlist",
176
+ FINDING_FIELDS.includes("message") && FINDING_FIELDS.includes("category") && FINDING_FIELDS.includes("topFrames"));
177
+
178
+ check(
179
+ "toFindings maps the new fields",
180
+ toFindings([row()], options)[0].message === "Sequence contains no elements" &&
181
+ toFindings([row()], options)[0].category === "Pliny.Application.Agents.RunExecutor" &&
182
+ toFindings([row()], options)[0].topFrames.length >= 1);
183
+
161
184
  section("Rendering and the scanner");
162
185
 
163
186
  const rendered = render(toFindings([row()], options)[0], options);
@@ -175,13 +198,18 @@ check(
175
198
  "no build-machine path reaches the body",
176
199
  !rendered.body.includes("/home/vsts"));
177
200
 
178
- // Two ways to have no frames, and they ask the reader for different things. Pliny-Bot #191 was
201
+ // Two ways to have no own frames, and they ask the reader for different things. Pliny-Bot #191 was
179
202
  // filed with OWN_CODE_PREFIX set to "Pliny" and still said the repository had not said which
180
203
  // assemblies were its own -- it sent a reader to configure a knob that had been set the day
181
204
  // before. Only the unconfigured case was covered here, which is why the wording survived.
182
205
  check(
183
- "no prefix configured says which knob to set",
206
+ "no prefix configured but top frames exist shows top frames",
184
207
  render(toFindings([row()], { ...options, ownCodePrefix: "" })[0], { ...options, ownCodePrefix: "" })
208
+ .body.includes("Top stack frames"));
209
+
210
+ check(
211
+ "no prefix configured and no frames at all says which knob to set",
212
+ render(toFindings([row({ AnyDetails: [] })], { ...options, ownCodePrefix: "" })[0], { ...options, ownCodePrefix: "" })
185
213
  .body.includes("has not said which assemblies are its own"));
186
214
 
187
215
  check(
@@ -194,6 +222,28 @@ check(
194
222
  render(toFindings([row()], { ...options, ownCodePrefix: "NoSuchAssembly" })[0], { ...options, ownCodePrefix: "NoSuchAssembly" })
195
223
  .body.includes("No frame in this exception belongs to `NoSuchAssembly`"));
196
224
 
225
+ check("body includes the message", rendered.body.includes("Message: `Sequence contains no elements`"));
226
+
227
+ check("body includes the source", rendered.body.includes("Source: `Pliny.Application.Agents.RunExecutor`"));
228
+
229
+ check(
230
+ "title uses category as fallback",
231
+ render(toFindings([row({ OperationName: "" })], options)[0], options).title ===
232
+ "System.InvalidOperationException in Pliny.Application.Agents.RunExecutor");
233
+
234
+ check("body shows own frames when available", rendered.body.includes("Our own frames"));
235
+
236
+ check(
237
+ "body shows top frames when no own frames",
238
+ render(toFindings([row({
239
+ ProblemId: "System.Threading.Tasks.TaskCanceledException",
240
+ ExceptionType: "System.Threading.Tasks.TaskCanceledException",
241
+ AnyDetails: [{ parsedStack: [{ method: "System.Net.Http.HttpClient.SendAsync" }] }],
242
+ AnyMessage: "A task was canceled.",
243
+ })], { ...options, ownCodePrefix: "Pliny" })[0],
244
+ { ...options, ownCodePrefix: "Pliny" })
245
+ .body.includes("Top stack frames"));
246
+
197
247
  section("Fail-closed, but not fail-dead");
198
248
 
199
249
  const mixed = prepare(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "0.28.6",
3
+ "version": "0.28.7",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",