arc-1 0.6.10 → 0.7.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 +8 -7
- package/bin/arc1-cli.js +10 -0
- package/bin/arc1.js +1 -1
- package/dist/adt/cds-impact.d.ts +35 -0
- package/dist/adt/cds-impact.d.ts.map +1 -1
- package/dist/adt/cds-impact.js +71 -0
- package/dist/adt/cds-impact.js.map +1 -1
- package/dist/adt/client.d.ts +4 -1
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +18 -5
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +32 -5
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/devtools.d.ts +39 -3
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +237 -25
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/diagnostics.d.ts +69 -7
- package/dist/adt/diagnostics.d.ts.map +1 -1
- package/dist/adt/diagnostics.js +694 -36
- package/dist/adt/diagnostics.js.map +1 -1
- package/dist/adt/errors.d.ts +14 -1
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +40 -9
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +86 -1
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/rap-handlers.d.ts +165 -0
- package/dist/adt/rap-handlers.d.ts.map +1 -0
- package/dist/adt/rap-handlers.js +835 -0
- package/dist/adt/rap-handlers.js.map +1 -0
- package/dist/adt/rap-preflight.d.ts +43 -0
- package/dist/adt/rap-preflight.d.ts.map +1 -0
- package/dist/adt/rap-preflight.js +405 -0
- package/dist/adt/rap-preflight.js.map +1 -0
- package/dist/adt/safety.d.ts +60 -36
- package/dist/adt/safety.d.ts.map +1 -1
- package/dist/adt/safety.js +202 -120
- package/dist/adt/safety.js.map +1 -1
- package/dist/adt/transport.d.ts +1 -1
- package/dist/adt/transport.js +2 -2
- package/dist/adt/transport.js.map +1 -1
- package/dist/adt/types.d.ts +88 -0
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/xml-parser.d.ts +13 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +26 -15
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/authz/policy.d.ts +53 -0
- package/dist/authz/policy.d.ts.map +1 -0
- package/dist/authz/policy.js +199 -0
- package/dist/authz/policy.js.map +1 -0
- package/dist/cli-args.d.ts +14 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +62 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli.d.ts +13 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +252 -55
- package/dist/cli.js.map +1 -1
- package/dist/extract-sap-cookies.d.ts +24 -0
- package/dist/extract-sap-cookies.d.ts.map +1 -0
- package/dist/extract-sap-cookies.js +317 -0
- package/dist/extract-sap-cookies.js.map +1 -0
- package/dist/handlers/hyperfocused.d.ts +4 -3
- package/dist/handlers/hyperfocused.d.ts.map +1 -1
- package/dist/handlers/hyperfocused.js +25 -16
- package/dist/handlers/hyperfocused.js.map +1 -1
- package/dist/handlers/intent.d.ts +4 -12
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +1238 -114
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +38 -10
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +69 -4
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +251 -164
- package/dist/handlers/tools.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/server/audit.d.ts +26 -3
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js.map +1 -1
- package/dist/server/config.d.ts +34 -19
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +320 -193
- package/dist/server/config.js.map +1 -1
- package/dist/server/deny-actions.d.ts +31 -0
- package/dist/server/deny-actions.d.ts.map +1 -0
- package/dist/server/deny-actions.js +156 -0
- package/dist/server/deny-actions.js.map +1 -0
- package/dist/server/effective-policy-log.d.ts +27 -0
- package/dist/server/effective-policy-log.d.ts.map +1 -0
- package/dist/server/effective-policy-log.js +103 -0
- package/dist/server/effective-policy-log.js.map +1 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +15 -16
- package/dist/server/http.js.map +1 -1
- package/dist/server/server.d.ts +37 -3
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +231 -30
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +29 -13
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +10 -11
- package/dist/server/types.js.map +1 -1
- package/dist/server/xsuaa.d.ts +1 -2
- package/dist/server/xsuaa.d.ts.map +1 -1
- package/dist/server/xsuaa.js +13 -14
- package/dist/server/xsuaa.js.map +1 -1
- package/package.json +6 -3
package/dist/adt/diagnostics.js
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { checkOperation, OperationType } from './safety.js';
|
|
11
11
|
import { findDeepNodes, parseXml } from './xml-parser.js';
|
|
12
|
+
// ─── Short Dumps ────────────────────────────────────────────────────
|
|
13
|
+
const DEFAULT_DUMP_MAX_RESULTS = 50;
|
|
14
|
+
const DEFAULT_SYSTEM_MESSAGE_MAX_RESULTS = 50;
|
|
15
|
+
const DEFAULT_GATEWAY_ERROR_MAX_RESULTS = 50;
|
|
16
|
+
const MAX_RESULTS_CAP = 200;
|
|
12
17
|
/**
|
|
13
18
|
* List ABAP short dumps (ST22 equivalent).
|
|
14
19
|
*
|
|
@@ -17,14 +22,7 @@ import { findDeepNodes, parseXml } from './xml-parser.js';
|
|
|
17
22
|
*/
|
|
18
23
|
export async function listDumps(http, safety, options) {
|
|
19
24
|
checkOperation(safety, OperationType.Read, 'ListDumps');
|
|
20
|
-
const
|
|
21
|
-
if (options?.maxResults) {
|
|
22
|
-
params.push(`$top=${options.maxResults}`);
|
|
23
|
-
}
|
|
24
|
-
if (options?.user) {
|
|
25
|
-
params.push(`$query=${encodeURIComponent(`and(equals(user,${options.user}))`)}`);
|
|
26
|
-
}
|
|
27
|
-
const queryString = params.length > 0 ? `?${params.join('&')}` : '';
|
|
25
|
+
const queryString = buildFeedQueryString(options, DEFAULT_DUMP_MAX_RESULTS, 'user');
|
|
28
26
|
const resp = await http.get(`/sap/bc/adt/runtime/dumps${queryString}`, {
|
|
29
27
|
Accept: 'application/atom+xml;type=feed',
|
|
30
28
|
});
|
|
@@ -52,6 +50,55 @@ export async function getDump(http, safety, dumpId) {
|
|
|
52
50
|
]);
|
|
53
51
|
return parseDumpDetail(xmlResp.body, textResp.body, dumpId);
|
|
54
52
|
}
|
|
53
|
+
// ─── System Messages + Gateway Errors ──────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* List SM02 system messages.
|
|
56
|
+
*
|
|
57
|
+
* Endpoint: GET /sap/bc/adt/runtime/systemmessages
|
|
58
|
+
* Returns an Atom feed with system message entries.
|
|
59
|
+
*/
|
|
60
|
+
export async function listSystemMessages(http, safety, options) {
|
|
61
|
+
checkOperation(safety, OperationType.Read, 'ListSystemMessages');
|
|
62
|
+
const queryString = buildFeedQueryString(options, DEFAULT_SYSTEM_MESSAGE_MAX_RESULTS, 'user');
|
|
63
|
+
const resp = await http.get(`/sap/bc/adt/runtime/systemmessages${queryString}`, {
|
|
64
|
+
Accept: 'application/atom+xml;type=feed',
|
|
65
|
+
});
|
|
66
|
+
return parseSystemMessages(resp.body);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* List SAP Gateway error log entries (/IWFND/ERROR_LOG).
|
|
70
|
+
*
|
|
71
|
+
* Endpoint: GET /sap/bc/adt/gw/errorlog
|
|
72
|
+
* Returns an Atom feed with gateway error entries.
|
|
73
|
+
*/
|
|
74
|
+
export async function listGatewayErrors(http, safety, options) {
|
|
75
|
+
checkOperation(safety, OperationType.Read, 'ListGatewayErrors');
|
|
76
|
+
const queryString = buildFeedQueryString(options, DEFAULT_GATEWAY_ERROR_MAX_RESULTS, 'username');
|
|
77
|
+
const resp = await http.get(`/sap/bc/adt/gw/errorlog${queryString}`, {
|
|
78
|
+
Accept: 'application/atom+xml;type=feed',
|
|
79
|
+
});
|
|
80
|
+
return parseGatewayErrors(resp.body);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Read one gateway error detail payload.
|
|
84
|
+
*
|
|
85
|
+
* The ADT /sap/bc/adt/gw/errorlog/{type}/{id} endpoint returns an HTML
|
|
86
|
+
* fragment (not XML), so the parser extracts tabular values from known
|
|
87
|
+
* section anchors (#HEADER, #SERVICE, #CONTEXT, #SOURCE, #STACK).
|
|
88
|
+
*
|
|
89
|
+
* Supports either:
|
|
90
|
+
* - full/relative ADT detail URL from a feed entry,
|
|
91
|
+
* - id of the form "{errorType}/{transactionId}" (as emitted by the feed), or
|
|
92
|
+
* - transaction id + errorType parameters.
|
|
93
|
+
*/
|
|
94
|
+
export async function getGatewayErrorDetail(http, safety, params) {
|
|
95
|
+
checkOperation(safety, OperationType.Read, 'GetGatewayErrorDetail');
|
|
96
|
+
const path = resolveGatewayErrorDetailPath(params);
|
|
97
|
+
const resp = await http.get(path, {
|
|
98
|
+
Accept: 'text/html, application/xhtml+xml, application/xml;q=0.5',
|
|
99
|
+
});
|
|
100
|
+
return parseGatewayErrorDetail(resp.body);
|
|
101
|
+
}
|
|
55
102
|
// ─── ABAP Traces ────────────────────────────────────────────────────
|
|
56
103
|
/**
|
|
57
104
|
* List ABAP profiler trace files.
|
|
@@ -106,43 +153,22 @@ export async function getTraceDbAccesses(http, safety, traceId) {
|
|
|
106
153
|
/**
|
|
107
154
|
* Parse dump listing Atom feed.
|
|
108
155
|
*
|
|
109
|
-
*
|
|
110
|
-
* - atom:author/atom:name → user
|
|
111
|
-
* - atom:category term="..." label="ABAP runtime error" → error type
|
|
112
|
-
* - atom:category term="..." label="Terminated ABAP program" → program
|
|
113
|
-
* - atom:published → timestamp
|
|
114
|
-
* - atom:link rel="self" href → contains dump ID path
|
|
156
|
+
* Robust against localized category labels and missing self links.
|
|
115
157
|
*/
|
|
116
158
|
export function parseDumpList(xml) {
|
|
117
159
|
const parsed = parseXml(xml);
|
|
118
160
|
const entryNodes = findDeepNodes(parsed, 'entry');
|
|
119
161
|
return entryNodes
|
|
120
162
|
.map((entry) => {
|
|
121
|
-
|
|
122
|
-
const author = entry.author;
|
|
163
|
+
const author = toRecordArray(entry.author)[0];
|
|
123
164
|
const user = String(author?.name ?? '');
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
let program = '';
|
|
129
|
-
for (const cat of categories) {
|
|
130
|
-
const label = String(cat['@_label'] ?? '');
|
|
131
|
-
if (label === 'ABAP runtime error')
|
|
132
|
-
error = String(cat['@_term'] ?? '');
|
|
133
|
-
if (label === 'Terminated ABAP program')
|
|
134
|
-
program = String(cat['@_term'] ?? '');
|
|
135
|
-
}
|
|
136
|
-
const timestamp = String(entry.published ?? '');
|
|
137
|
-
// Extract dump ID from self link href
|
|
138
|
-
const links = Array.isArray(entry.link) ? entry.link : entry.link ? [entry.link] : [];
|
|
139
|
-
const selfLink = links.find((l) => String(l['@_rel'] ?? '') === 'self');
|
|
140
|
-
const href = String(selfLink?.['@_href'] ?? '');
|
|
141
|
-
const dumpMatch = href.match(/\/sap\/bc\/adt\/runtime\/dump\/([^"]*)/);
|
|
142
|
-
const id = dumpMatch?.[1] || '';
|
|
165
|
+
const categories = toRecordArray(entry.category);
|
|
166
|
+
const { error, program } = parseDumpCategories(categories);
|
|
167
|
+
const timestamp = String(entry.published ?? entry.updated ?? '');
|
|
168
|
+
const id = extractDumpId(entry);
|
|
143
169
|
return { id, timestamp, user, error, program };
|
|
144
170
|
})
|
|
145
|
-
.filter((
|
|
171
|
+
.filter((entry) => entry.id.length > 0);
|
|
146
172
|
}
|
|
147
173
|
/**
|
|
148
174
|
* Parse dump detail XML metadata + formatted text.
|
|
@@ -171,7 +197,11 @@ export function parseDumpDetail(xml, formattedText, dumpId) {
|
|
|
171
197
|
name: String(ch['@_name'] ?? ''),
|
|
172
198
|
title: String(ch['@_title'] ?? ''),
|
|
173
199
|
category: String(ch['@_category'] ?? ''),
|
|
200
|
+
line: safePositiveInt(ch['@_line']),
|
|
201
|
+
chapterOrder: safePositiveInt(ch['@_chapterOrder']),
|
|
202
|
+
categoryOrder: safePositiveInt(ch['@_categoryOrder']),
|
|
174
203
|
}));
|
|
204
|
+
const sections = splitDumpSections(formattedText, chapters);
|
|
175
205
|
return {
|
|
176
206
|
id: dumpId,
|
|
177
207
|
error,
|
|
@@ -181,9 +211,200 @@ export function parseDumpDetail(xml, formattedText, dumpId) {
|
|
|
181
211
|
timestamp,
|
|
182
212
|
chapters,
|
|
183
213
|
formattedText,
|
|
214
|
+
sections,
|
|
184
215
|
terminationUri,
|
|
185
216
|
};
|
|
186
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Parse system message feed.
|
|
220
|
+
*/
|
|
221
|
+
export function parseSystemMessages(xml) {
|
|
222
|
+
const parsed = parseXml(xml);
|
|
223
|
+
const entryNodes = findDeepNodes(parsed, 'entry');
|
|
224
|
+
return entryNodes
|
|
225
|
+
.map((entry) => {
|
|
226
|
+
const links = toRecordArray(entry.link);
|
|
227
|
+
const selfHref = extractSelfLinkHref(links);
|
|
228
|
+
const categories = toRecordArray(entry.category);
|
|
229
|
+
const severity = String(categories[0]?.['@_term'] ?? '');
|
|
230
|
+
const contentNode = toRecordArray(entry.content)[0];
|
|
231
|
+
const summaryNode = toRecordArray(entry.summary)[0];
|
|
232
|
+
return {
|
|
233
|
+
id: String(entry.id ?? ''),
|
|
234
|
+
title: String(entry.title ?? ''),
|
|
235
|
+
text: String(contentNode?.['#text'] ?? summaryNode?.['#text'] ?? entry.summary ?? ''),
|
|
236
|
+
severity,
|
|
237
|
+
validFrom: String(entry['@_validFrom'] ?? entry.validFrom ?? entry.updated ?? entry.published ?? ''),
|
|
238
|
+
validTo: String(entry['@_validTo'] ?? entry.validTo ?? ''),
|
|
239
|
+
createdBy: String(toRecordArray(entry.author)[0]?.name ?? ''),
|
|
240
|
+
timestamp: String(entry.updated ?? entry.published ?? ''),
|
|
241
|
+
detailUrl: selfHref || undefined,
|
|
242
|
+
};
|
|
243
|
+
})
|
|
244
|
+
.filter((entry) => entry.id.length > 0 || entry.title.length > 0 || entry.text.length > 0);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Parse gateway error log feed.
|
|
248
|
+
*
|
|
249
|
+
* Real ADT feed entries encode the error class + transaction id in
|
|
250
|
+
* <atom:id>ErrorClass/transactionId</atom:id>, the full label in
|
|
251
|
+
* <atom:title>Type: short text</atom:title>, and the structured payload in
|
|
252
|
+
* the <atom:summary type="html"> HTML blob (same content the detail
|
|
253
|
+
* endpoint returns). No <atom:category> or <atom:link rel="self"> is
|
|
254
|
+
* emitted, so the parser derives the detail URL from the atom:id and
|
|
255
|
+
* extracts header fields from the summary HTML when available.
|
|
256
|
+
*/
|
|
257
|
+
export function parseGatewayErrors(xml) {
|
|
258
|
+
const parsed = parseXml(xml);
|
|
259
|
+
const entryNodes = findDeepNodes(parsed, 'entry');
|
|
260
|
+
return entryNodes
|
|
261
|
+
.map((entry) => {
|
|
262
|
+
const atomId = String(entry.id ?? '');
|
|
263
|
+
const rawTitle = String(entry.title ?? '').trim();
|
|
264
|
+
const summaryHtml = extractEntrySummaryHtml(entry);
|
|
265
|
+
const { errorType: idErrorType, transactionId: idTransactionId } = splitGatewayAtomId(atomId);
|
|
266
|
+
const links = toRecordArray(entry.link);
|
|
267
|
+
const selfHref = extractSelfLinkHref(links);
|
|
268
|
+
// Legacy / forward-compat: some feeds may expose <atom:category term="Frontend Error"/>
|
|
269
|
+
const categoryTerm = String(toRecordArray(entry.category)[0]?.['@_term'] ?? '').trim();
|
|
270
|
+
// Multi-source derivation so one missing field does not lose everything.
|
|
271
|
+
const summaryType = extractHtmlHeaderValue(summaryHtml, 'Type');
|
|
272
|
+
const titleType = rawTitle.includes(':') ? rawTitle.slice(0, rawTitle.indexOf(':')).trim() : '';
|
|
273
|
+
const typeFromId = splitCamelCase(idErrorType);
|
|
274
|
+
const type = summaryType || categoryTerm || titleType || typeFromId;
|
|
275
|
+
const summaryShortText = extractHtmlHeaderValue(summaryHtml, 'Short Text');
|
|
276
|
+
const titleShortText = rawTitle.includes(':') ? rawTitle.slice(rawTitle.indexOf(':') + 1).trim() : rawTitle;
|
|
277
|
+
const shortText = summaryShortText || titleShortText;
|
|
278
|
+
const summaryTransactionId = extractTransactionIdFromHtml(summaryHtml);
|
|
279
|
+
const transactionId = summaryTransactionId || idTransactionId || extractTailId(atomId);
|
|
280
|
+
const detailUrl = selfHref ||
|
|
281
|
+
(idErrorType && idTransactionId
|
|
282
|
+
? `/sap/bc/adt/gw/errorlog/${encodeURIComponent(idErrorType)}/${encodeURIComponent(idTransactionId)}`
|
|
283
|
+
: '');
|
|
284
|
+
return {
|
|
285
|
+
type,
|
|
286
|
+
shortText,
|
|
287
|
+
transactionId,
|
|
288
|
+
dateTime: String(entry.updated ?? entry.published ?? ''),
|
|
289
|
+
username: String(toRecordArray(entry.author)[0]?.name ?? ''),
|
|
290
|
+
detailUrl,
|
|
291
|
+
package: getOptionalString(entry, ['@_package', 'package']) ??
|
|
292
|
+
(extractHtmlHeaderValue(summaryHtml, 'Package') || undefined),
|
|
293
|
+
applicationComponent: getOptionalString(entry, ['@_applicationComponent', 'applicationComponent']) ??
|
|
294
|
+
(extractHtmlHeaderValue(summaryHtml, 'Application Component') || undefined),
|
|
295
|
+
client: getOptionalString(entry, ['@_client', 'client']) ??
|
|
296
|
+
(extractHtmlHeaderValue(summaryHtml, 'Client') || undefined),
|
|
297
|
+
requestKind: getOptionalString(entry, ['@_requestKind', 'requestKind']) ??
|
|
298
|
+
(extractHtmlHeaderValue(summaryHtml, 'Request Kind') || undefined),
|
|
299
|
+
};
|
|
300
|
+
})
|
|
301
|
+
.filter((entry) => entry.transactionId.length > 0 || entry.detailUrl.length > 0);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Parse gateway error detail payload.
|
|
305
|
+
*
|
|
306
|
+
* Accepts either the legacy XML envelope (with <errorEntry>) if the backend
|
|
307
|
+
* ever returns one, or the HTML fragment that the real /sap/bc/adt/gw/errorlog
|
|
308
|
+
* endpoint returns. Missing sections fall back to empty values rather than
|
|
309
|
+
* throwing, so callers can still surface partial data to the LLM.
|
|
310
|
+
*/
|
|
311
|
+
export function parseGatewayErrorDetail(payload) {
|
|
312
|
+
const trimmed = (payload ?? '').trim();
|
|
313
|
+
const looksLikeXmlEnvelope = trimmed.startsWith('<?xml') || /<errorEntry[\s>]/.test(trimmed);
|
|
314
|
+
if (looksLikeXmlEnvelope) {
|
|
315
|
+
const xmlResult = parseGatewayErrorDetailXml(trimmed);
|
|
316
|
+
if (xmlResult)
|
|
317
|
+
return xmlResult;
|
|
318
|
+
}
|
|
319
|
+
return parseGatewayErrorDetailHtml(trimmed);
|
|
320
|
+
}
|
|
321
|
+
function parseGatewayErrorDetailXml(xml) {
|
|
322
|
+
try {
|
|
323
|
+
const parsed = parseXml(xml);
|
|
324
|
+
const errorNode = findDeepNodes(parsed, 'errorEntry')[0];
|
|
325
|
+
if (!errorNode)
|
|
326
|
+
return undefined;
|
|
327
|
+
const callStackEntries = parseGatewayCallStack(errorNode);
|
|
328
|
+
const sourceLines = parseGatewaySourceLines(errorNode);
|
|
329
|
+
const exceptions = parseGatewayExceptions(errorNode);
|
|
330
|
+
const serviceInfoNode = toRecordArray(errorNode.serviceInfo)[0];
|
|
331
|
+
const errorContextNode = toRecordArray(errorNode.errorContext)[0];
|
|
332
|
+
const sourceCodeNode = toRecordArray(errorNode.sourceCode)[0];
|
|
333
|
+
const serviceInfo = {
|
|
334
|
+
namespace: String(serviceInfoNode?.['@_namespace'] ?? ''),
|
|
335
|
+
serviceName: String(serviceInfoNode?.['@_serviceName'] ?? ''),
|
|
336
|
+
serviceVersion: String(serviceInfoNode?.['@_serviceVersion'] ?? ''),
|
|
337
|
+
groupId: String(serviceInfoNode?.['@_groupId'] ?? ''),
|
|
338
|
+
serviceRepository: String(serviceInfoNode?.['@_serviceRepository'] ?? ''),
|
|
339
|
+
destination: String(serviceInfoNode?.['@_destination'] ?? ''),
|
|
340
|
+
};
|
|
341
|
+
return {
|
|
342
|
+
type: String(errorNode['@_type'] ?? ''),
|
|
343
|
+
shortText: String(errorNode.shortText ?? ''),
|
|
344
|
+
transactionId: String(errorNode.transactionId ?? ''),
|
|
345
|
+
package: String(errorNode.package ?? ''),
|
|
346
|
+
applicationComponent: String(errorNode.applicationComponent ?? ''),
|
|
347
|
+
dateTime: String(errorNode.dateTime ?? ''),
|
|
348
|
+
username: String(errorNode.username ?? ''),
|
|
349
|
+
client: String(errorNode.client ?? ''),
|
|
350
|
+
requestKind: String(errorNode.requestKind ?? ''),
|
|
351
|
+
serviceInfo,
|
|
352
|
+
errorContext: {
|
|
353
|
+
errorInfo: String(errorContextNode?.errorInfo ?? ''),
|
|
354
|
+
resolution: {},
|
|
355
|
+
exceptions,
|
|
356
|
+
},
|
|
357
|
+
sourceCode: {
|
|
358
|
+
lines: sourceLines,
|
|
359
|
+
errorLine: safePositiveInt(sourceCodeNode?.['@_errorLine']),
|
|
360
|
+
},
|
|
361
|
+
callStack: callStackEntries,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function parseGatewayErrorDetailHtml(html) {
|
|
369
|
+
const header = extractHtmlSection(html, 'HEADER');
|
|
370
|
+
const service = extractHtmlSection(html, 'SERVICE');
|
|
371
|
+
const context = extractHtmlSection(html, 'CONTEXT');
|
|
372
|
+
const source = extractHtmlSection(html, 'SOURCE');
|
|
373
|
+
const stack = extractHtmlSection(html, 'STACK');
|
|
374
|
+
const resolution = {};
|
|
375
|
+
const sapNote = extractHtmlHeaderValue(context, 'SAP_NOTE');
|
|
376
|
+
if (sapNote)
|
|
377
|
+
resolution.sapNote = sapNote;
|
|
378
|
+
const sapNoteLink = extractHtmlHeaderValue(context, 'LINK_TO_SAP_NOTE');
|
|
379
|
+
if (sapNoteLink)
|
|
380
|
+
resolution.linkToSapNote = sapNoteLink;
|
|
381
|
+
return {
|
|
382
|
+
type: extractHtmlHeaderValue(header, 'Type'),
|
|
383
|
+
shortText: extractHtmlHeaderValue(header, 'Short Text'),
|
|
384
|
+
transactionId: extractTransactionIdFromHtml(header),
|
|
385
|
+
package: extractHtmlHeaderValue(header, 'Package'),
|
|
386
|
+
applicationComponent: extractHtmlHeaderValue(header, 'Application Component'),
|
|
387
|
+
dateTime: extractHtmlHeaderValue(header, 'Date/Time'),
|
|
388
|
+
username: extractHtmlHeaderValue(header, 'Username'),
|
|
389
|
+
client: extractHtmlHeaderValue(header, 'Client'),
|
|
390
|
+
requestKind: extractHtmlHeaderValue(header, 'Request Kind'),
|
|
391
|
+
serviceInfo: {
|
|
392
|
+
namespace: extractHtmlHeaderValue(service, 'Service Namespace'),
|
|
393
|
+
serviceName: extractHtmlHeaderValue(service, 'Service Name'),
|
|
394
|
+
serviceVersion: extractHtmlHeaderValue(service, 'Service Version'),
|
|
395
|
+
groupId: extractHtmlHeaderValue(service, 'Group ID'),
|
|
396
|
+
serviceRepository: extractHtmlHeaderValue(service, 'Service Repository'),
|
|
397
|
+
destination: extractHtmlHeaderValue(service, 'Destination'),
|
|
398
|
+
},
|
|
399
|
+
errorContext: {
|
|
400
|
+
errorInfo: extractHtmlHeaderValue(context, 'ERROR_INFO'),
|
|
401
|
+
resolution,
|
|
402
|
+
exceptions: extractGatewayExceptionsFromHtml(context),
|
|
403
|
+
},
|
|
404
|
+
sourceCode: extractGatewaySourceFromHtml(source),
|
|
405
|
+
callStack: extractGatewayCallStackFromHtml(stack),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
187
408
|
/**
|
|
188
409
|
* Parse trace listing Atom feed.
|
|
189
410
|
*
|
|
@@ -267,4 +488,441 @@ export function parseTraceDbAccesses(xml) {
|
|
|
267
488
|
accessTime: Number(node['@_accessTime'] ?? 0),
|
|
268
489
|
}));
|
|
269
490
|
}
|
|
491
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
492
|
+
function buildFeedQueryString(options, defaultMaxResults, userAttribute) {
|
|
493
|
+
const params = [];
|
|
494
|
+
const maxResults = clampMaxResults(options?.maxResults, defaultMaxResults);
|
|
495
|
+
params.push(`$top=${maxResults}`);
|
|
496
|
+
const user = String(options?.user ?? '').trim();
|
|
497
|
+
if (user) {
|
|
498
|
+
params.push(`$query=${encodeURIComponent(`and(equals(${userAttribute},${user}))`)}`);
|
|
499
|
+
}
|
|
500
|
+
const from = String(options?.from ?? '').trim();
|
|
501
|
+
if (from)
|
|
502
|
+
params.push(`from=${encodeURIComponent(from)}`);
|
|
503
|
+
const to = String(options?.to ?? '').trim();
|
|
504
|
+
if (to)
|
|
505
|
+
params.push(`to=${encodeURIComponent(to)}`);
|
|
506
|
+
return params.length > 0 ? `?${params.join('&')}` : '';
|
|
507
|
+
}
|
|
508
|
+
function clampMaxResults(maxResults, fallback) {
|
|
509
|
+
if (!Number.isFinite(maxResults))
|
|
510
|
+
return fallback;
|
|
511
|
+
return Math.max(1, Math.min(MAX_RESULTS_CAP, Math.trunc(maxResults)));
|
|
512
|
+
}
|
|
513
|
+
function toRecordArray(value) {
|
|
514
|
+
if (Array.isArray(value)) {
|
|
515
|
+
return value.filter((entry) => Boolean(entry) && typeof entry === 'object');
|
|
516
|
+
}
|
|
517
|
+
if (value && typeof value === 'object')
|
|
518
|
+
return [value];
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
function safePositiveInt(value) {
|
|
522
|
+
const parsed = Number(value);
|
|
523
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : 0;
|
|
524
|
+
}
|
|
525
|
+
function normalizeLabel(label) {
|
|
526
|
+
return label.toLowerCase().replace(/[_-]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
527
|
+
}
|
|
528
|
+
function parseDumpCategories(categories) {
|
|
529
|
+
const normalized = categories
|
|
530
|
+
.map((category) => ({
|
|
531
|
+
term: String(category['@_term'] ?? ''),
|
|
532
|
+
label: normalizeLabel(String(category['@_label'] ?? '')),
|
|
533
|
+
}))
|
|
534
|
+
.filter((entry) => entry.term.length > 0);
|
|
535
|
+
if (normalized.length === 0)
|
|
536
|
+
return { error: '', program: '' };
|
|
537
|
+
const errorByLabel = normalized.find((entry) => entry.label.includes('runtime error') || (entry.label.includes('error') && !entry.label.includes('program')))?.term;
|
|
538
|
+
const programByLabel = normalized.find((entry) => entry.label.includes('program'))?.term;
|
|
539
|
+
const fallbackError = normalized[0]?.term ?? '';
|
|
540
|
+
const fallbackProgram = normalized[1]?.term ?? normalized.find((entry) => entry.term !== fallbackError)?.term ?? '';
|
|
541
|
+
return {
|
|
542
|
+
error: errorByLabel ?? fallbackError,
|
|
543
|
+
program: programByLabel ?? fallbackProgram,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function extractSelfLinkHref(links) {
|
|
547
|
+
const selfLink = links.find((link) => String(link['@_rel'] ?? '') === 'self');
|
|
548
|
+
return String(selfLink?.['@_href'] ?? links[0]?.['@_href'] ?? '');
|
|
549
|
+
}
|
|
550
|
+
function extractDumpId(entry) {
|
|
551
|
+
const links = toRecordArray(entry.link);
|
|
552
|
+
const selfHref = extractSelfLinkHref(links);
|
|
553
|
+
const fromLink = extractIdFromPath(selfHref, ['/runtime/dump/']);
|
|
554
|
+
if (fromLink)
|
|
555
|
+
return fromLink;
|
|
556
|
+
const atomId = String(entry.id ?? '');
|
|
557
|
+
const fromAtomId = extractIdFromPath(atomId, ['/runtime/dump/', '/runtime/dumps/']);
|
|
558
|
+
if (fromAtomId)
|
|
559
|
+
return fromAtomId;
|
|
560
|
+
const serialized = JSON.stringify(entry);
|
|
561
|
+
const fallback = serialized.match(/\/runtime\/dumps?\/([^"\\\s<]+)/)?.[1] ?? '';
|
|
562
|
+
return fallback.trim();
|
|
563
|
+
}
|
|
564
|
+
function extractIdFromPath(rawPath, markers) {
|
|
565
|
+
const path = normalizeAdtPath(rawPath, false);
|
|
566
|
+
if (!path)
|
|
567
|
+
return '';
|
|
568
|
+
for (const marker of markers) {
|
|
569
|
+
const idx = path.indexOf(marker);
|
|
570
|
+
if (idx >= 0) {
|
|
571
|
+
const start = idx + marker.length;
|
|
572
|
+
const tail = path.slice(start);
|
|
573
|
+
const id = tail.split(/[/?#]/)[0] ?? '';
|
|
574
|
+
if (id.trim())
|
|
575
|
+
return id.trim();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return '';
|
|
579
|
+
}
|
|
580
|
+
function extractTailId(value) {
|
|
581
|
+
const normalized = normalizeAdtPath(value, false);
|
|
582
|
+
if (!normalized)
|
|
583
|
+
return value;
|
|
584
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
585
|
+
return parts[parts.length - 1] ?? value;
|
|
586
|
+
}
|
|
587
|
+
function splitDumpSections(formattedText, chapters) {
|
|
588
|
+
if (!formattedText)
|
|
589
|
+
return {};
|
|
590
|
+
const lines = formattedText.split(/\r?\n/);
|
|
591
|
+
const sortable = chapters
|
|
592
|
+
.filter((chapter) => chapter.line > 0)
|
|
593
|
+
.sort((a, b) => {
|
|
594
|
+
if (a.line !== b.line)
|
|
595
|
+
return a.line - b.line;
|
|
596
|
+
if (a.chapterOrder !== b.chapterOrder)
|
|
597
|
+
return a.chapterOrder - b.chapterOrder;
|
|
598
|
+
return a.name.localeCompare(b.name);
|
|
599
|
+
});
|
|
600
|
+
if (sortable.length === 0)
|
|
601
|
+
return {};
|
|
602
|
+
const sections = {};
|
|
603
|
+
for (let i = 0; i < sortable.length; i++) {
|
|
604
|
+
const chapter = sortable[i];
|
|
605
|
+
const next = sortable[i + 1];
|
|
606
|
+
const startLine = Math.max(0, chapter.line - 1);
|
|
607
|
+
const endLine = next?.line ? Math.max(startLine, next.line - 1) : lines.length;
|
|
608
|
+
const rawSection = lines.slice(startLine, endLine).join('\n').trim();
|
|
609
|
+
const normalized = shouldNormalizeWrappedLines(chapter) ? joinWrappedLines(rawSection) : rawSection;
|
|
610
|
+
const sectionId = chapter.name || `section_${i + 1}`;
|
|
611
|
+
sections[sectionId] = normalized;
|
|
612
|
+
}
|
|
613
|
+
return sections;
|
|
614
|
+
}
|
|
615
|
+
function shouldNormalizeWrappedLines(chapter) {
|
|
616
|
+
const title = normalizeLabel(chapter.title);
|
|
617
|
+
return (title.includes('source code') ||
|
|
618
|
+
title.includes('active calls') ||
|
|
619
|
+
title.includes('call stack') ||
|
|
620
|
+
title.includes('kernel'));
|
|
621
|
+
}
|
|
622
|
+
function joinWrappedLines(text) {
|
|
623
|
+
if (!text.includes('\\'))
|
|
624
|
+
return text;
|
|
625
|
+
const lines = text.split('\n');
|
|
626
|
+
const result = [];
|
|
627
|
+
for (const line of lines) {
|
|
628
|
+
if (result.length > 0 && result[result.length - 1].endsWith('\\')) {
|
|
629
|
+
const prev = result[result.length - 1];
|
|
630
|
+
result[result.length - 1] = `${prev.slice(0, -1)}${line.trimStart()}`;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
result.push(line);
|
|
634
|
+
}
|
|
635
|
+
return result.join('\n');
|
|
636
|
+
}
|
|
637
|
+
function getOptionalString(entry, keys) {
|
|
638
|
+
for (const key of keys) {
|
|
639
|
+
const value = entry[key];
|
|
640
|
+
if (value != null && String(value).trim().length > 0) {
|
|
641
|
+
return String(value);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
function parseGatewayCallStack(root) {
|
|
647
|
+
const callStackNode = toRecordArray(root.callStack)[0];
|
|
648
|
+
const entries = toRecordArray(callStackNode?.entry);
|
|
649
|
+
return entries.map((entry, index) => ({
|
|
650
|
+
number: safePositiveInt(entry['@_number']) || index + 1,
|
|
651
|
+
event: String(entry['@_event'] ?? ''),
|
|
652
|
+
program: String(entry['@_program'] ?? ''),
|
|
653
|
+
name: String(entry['@_name'] ?? ''),
|
|
654
|
+
line: safePositiveInt(entry['@_line']),
|
|
655
|
+
}));
|
|
656
|
+
}
|
|
657
|
+
function parseGatewaySourceLines(root) {
|
|
658
|
+
const sourceCodeNode = toRecordArray(root.sourceCode)[0];
|
|
659
|
+
const lines = toRecordArray(sourceCodeNode?.line);
|
|
660
|
+
return lines.map((line, index) => ({
|
|
661
|
+
number: safePositiveInt(line['@_number']) || index + 1,
|
|
662
|
+
content: typeof line['#text'] === 'string' ? line['#text'] : String(line ?? ''),
|
|
663
|
+
isError: String(line['@_isError'] ?? '').toLowerCase() === 'true',
|
|
664
|
+
}));
|
|
665
|
+
}
|
|
666
|
+
function parseGatewayExceptions(root) {
|
|
667
|
+
const errorContextNode = toRecordArray(root.errorContext)[0];
|
|
668
|
+
const exceptionsNode = toRecordArray(errorContextNode?.exceptions)[0];
|
|
669
|
+
const exceptions = toRecordArray(exceptionsNode?.exception);
|
|
670
|
+
return exceptions.map((entry) => ({
|
|
671
|
+
type: String(entry['@_type'] ?? ''),
|
|
672
|
+
text: String(entry['#text'] ?? ''),
|
|
673
|
+
raiseLocation: String(entry['@_raiseLocation'] ?? ''),
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
676
|
+
function resolveGatewayErrorDetailPath(params) {
|
|
677
|
+
const detailUrl = String(params.detailUrl ?? '').trim();
|
|
678
|
+
if (detailUrl) {
|
|
679
|
+
return normalizeAdtPath(detailUrl, true);
|
|
680
|
+
}
|
|
681
|
+
const id = String(params.id ?? '').trim();
|
|
682
|
+
if (!id) {
|
|
683
|
+
throw new Error('Gateway error detail requires either "detailUrl" or "id" with "errorType".');
|
|
684
|
+
}
|
|
685
|
+
if (id.includes('/sap/bc/adt/')) {
|
|
686
|
+
return normalizeAdtPath(id, true);
|
|
687
|
+
}
|
|
688
|
+
// Feed atom:id is emitted as "{errorType}/{transactionId}" — accept that form directly.
|
|
689
|
+
if (id.includes('/') && !params.errorType) {
|
|
690
|
+
const [derivedType, ...rest] = id.split('/');
|
|
691
|
+
const derivedId = rest.join('/');
|
|
692
|
+
if (derivedType && derivedId) {
|
|
693
|
+
return `/sap/bc/adt/gw/errorlog/${encodeURIComponent(decodeUriComponentSafe(derivedType))}/${encodeURIComponent(decodeUriComponentSafe(derivedId))}`;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const errorType = String(params.errorType ?? '').trim();
|
|
697
|
+
if (!errorType) {
|
|
698
|
+
throw new Error('Gateway error detail by transaction ID requires "errorType".');
|
|
699
|
+
}
|
|
700
|
+
// Feed returns display form "Frontend Error" (with space) in atom:title, but the
|
|
701
|
+
// detail URL path expects the compact identifier form "FrontendError". Strip
|
|
702
|
+
// whitespace to allow callers to pass either shape.
|
|
703
|
+
const normalizedType = errorType.replace(/\s+/g, '');
|
|
704
|
+
return `/sap/bc/adt/gw/errorlog/${encodeURIComponent(normalizedType)}/${encodeURIComponent(decodeUriComponentSafe(id))}`;
|
|
705
|
+
}
|
|
706
|
+
function normalizeAdtPath(rawPath, requireAdtPrefix) {
|
|
707
|
+
if (!rawPath)
|
|
708
|
+
return '';
|
|
709
|
+
const trimmed = rawPath.trim();
|
|
710
|
+
let normalized = trimmed;
|
|
711
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
712
|
+
try {
|
|
713
|
+
const url = new URL(trimmed);
|
|
714
|
+
normalized = `${url.pathname}${url.search}`;
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
normalized = trimmed;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (/^adt:\/\//i.test(normalized)) {
|
|
721
|
+
const marker = normalized.indexOf('/sap/bc/adt/');
|
|
722
|
+
if (marker >= 0) {
|
|
723
|
+
normalized = normalized.slice(marker);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (!normalized.startsWith('/') && normalized.includes('/sap/bc/adt/')) {
|
|
727
|
+
normalized = normalized.slice(normalized.indexOf('/sap/bc/adt/'));
|
|
728
|
+
}
|
|
729
|
+
if (requireAdtPrefix && !normalized.startsWith('/sap/bc/adt/')) {
|
|
730
|
+
throw new Error(`Unsupported ADT detail URL: ${rawPath}`);
|
|
731
|
+
}
|
|
732
|
+
return normalized;
|
|
733
|
+
}
|
|
734
|
+
// ─── Gateway HTML helpers ──────────────────────────────────────────
|
|
735
|
+
//
|
|
736
|
+
// The gateway error log detail endpoint returns an HTML fragment built
|
|
737
|
+
// from known section anchors. We extract tabular values with regex rather
|
|
738
|
+
// than a full HTML parser to keep the dependency surface small and stay
|
|
739
|
+
// resilient to whitespace/attribute variations across releases.
|
|
740
|
+
function splitGatewayAtomId(atomId) {
|
|
741
|
+
const cleaned = decodeHtmlEntities(String(atomId ?? '')).trim();
|
|
742
|
+
if (!cleaned)
|
|
743
|
+
return { errorType: '', transactionId: '' };
|
|
744
|
+
const marker = '/sap/bc/adt/gw/errorlog/';
|
|
745
|
+
if (cleaned.includes(marker)) {
|
|
746
|
+
const tail = cleaned.slice(cleaned.indexOf(marker) + marker.length);
|
|
747
|
+
const [errorType, ...rest] = tail.split('/');
|
|
748
|
+
return {
|
|
749
|
+
errorType: decodeUriComponentSafe(errorType ?? ''),
|
|
750
|
+
transactionId: decodeUriComponentSafe(rest.join('/') ?? ''),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
const slashIdx = cleaned.indexOf('/');
|
|
754
|
+
if (slashIdx >= 0) {
|
|
755
|
+
return {
|
|
756
|
+
errorType: decodeUriComponentSafe(cleaned.slice(0, slashIdx)),
|
|
757
|
+
transactionId: decodeUriComponentSafe(cleaned.slice(slashIdx + 1)),
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
return { errorType: '', transactionId: decodeUriComponentSafe(cleaned) };
|
|
761
|
+
}
|
|
762
|
+
function splitCamelCase(value) {
|
|
763
|
+
if (!value)
|
|
764
|
+
return '';
|
|
765
|
+
return value
|
|
766
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
767
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
768
|
+
.replace(/\s+/g, ' ')
|
|
769
|
+
.trim();
|
|
770
|
+
}
|
|
771
|
+
function extractEntrySummaryHtml(entry) {
|
|
772
|
+
const summary = entry.summary;
|
|
773
|
+
if (summary == null)
|
|
774
|
+
return '';
|
|
775
|
+
if (typeof summary === 'string')
|
|
776
|
+
return decodeHtmlEntities(summary);
|
|
777
|
+
const summaryNode = toRecordArray(summary)[0];
|
|
778
|
+
if (!summaryNode)
|
|
779
|
+
return '';
|
|
780
|
+
const text = summaryNode['#text'];
|
|
781
|
+
if (typeof text === 'string' && text.length > 0)
|
|
782
|
+
return decodeHtmlEntities(text);
|
|
783
|
+
return decodeHtmlEntities(String(summaryNode ?? ''));
|
|
784
|
+
}
|
|
785
|
+
function extractHtmlSection(html, anchorId) {
|
|
786
|
+
if (!html)
|
|
787
|
+
return '';
|
|
788
|
+
const startRe = new RegExp(`<h4[^>]*id="${escapeRegex(anchorId)}"[^>]*>`, 'i');
|
|
789
|
+
const start = html.search(startRe);
|
|
790
|
+
if (start < 0)
|
|
791
|
+
return '';
|
|
792
|
+
const rest = html.slice(start);
|
|
793
|
+
const nextH4 = rest.slice(1).search(/<h4[\s>]/i);
|
|
794
|
+
return nextH4 > 0 ? rest.slice(0, nextH4 + 1) : rest;
|
|
795
|
+
}
|
|
796
|
+
function extractHtmlHeaderValue(html, label) {
|
|
797
|
+
if (!html || !label)
|
|
798
|
+
return '';
|
|
799
|
+
const labelPattern = escapeRegex(label).replace(/_/g, '[_\\s]?');
|
|
800
|
+
const re = new RegExp(`<b[^>]*>\\s*(?: |\\s)*${labelPattern}\\s*</b>\\s*</td>\\s*<td[^>]*>([\\s\\S]*?)</td>`, 'i');
|
|
801
|
+
const match = html.match(re);
|
|
802
|
+
if (!match?.[1])
|
|
803
|
+
return '';
|
|
804
|
+
return sanitizeHtmlCellValue(match[1]);
|
|
805
|
+
}
|
|
806
|
+
function extractTransactionIdFromHtml(html) {
|
|
807
|
+
const raw = extractHtmlHeaderValue(html, 'Transaction ID');
|
|
808
|
+
if (!raw)
|
|
809
|
+
return '';
|
|
810
|
+
// Strip the "(Replay in GW Client)" link/suffix that SAP appends.
|
|
811
|
+
const firstToken = raw.split(/\s+/).find((part) => /^[A-Za-z0-9]{16,}$/.test(part));
|
|
812
|
+
return firstToken ?? raw;
|
|
813
|
+
}
|
|
814
|
+
function extractGatewayExceptionsFromHtml(contextHtml) {
|
|
815
|
+
if (!contextHtml)
|
|
816
|
+
return [];
|
|
817
|
+
const exceptionsIdx = contextHtml.search(/–\s*Exceptions\s*<\/b>/i);
|
|
818
|
+
const attributesIdx = contextHtml.search(/–\s*Attributes\s*<\/b>/i);
|
|
819
|
+
if (exceptionsIdx < 0)
|
|
820
|
+
return [];
|
|
821
|
+
const slice = contextHtml.slice(exceptionsIdx, attributesIdx > exceptionsIdx ? attributesIdx : contextHtml.length);
|
|
822
|
+
const exceptions = [];
|
|
823
|
+
const exceptionBlockRe = /<b[^>]*>[^<]*–\s*(\/?[^\s<]+)\s*<\/b>/g;
|
|
824
|
+
let match;
|
|
825
|
+
while ((match = exceptionBlockRe.exec(slice)) !== null) {
|
|
826
|
+
const name = (match[1] ?? '').trim();
|
|
827
|
+
if (!name || /^Exceptions$/i.test(name))
|
|
828
|
+
continue;
|
|
829
|
+
const afterIdx = match.index + match[0].length;
|
|
830
|
+
const block = slice.slice(afterIdx, afterIdx + 2500);
|
|
831
|
+
const text = extractHtmlHeaderValue(block, 'Text');
|
|
832
|
+
exceptions.push({ type: name, text, raiseLocation: '' });
|
|
833
|
+
}
|
|
834
|
+
return exceptions;
|
|
835
|
+
}
|
|
836
|
+
function extractGatewaySourceFromHtml(sourceHtml) {
|
|
837
|
+
if (!sourceHtml)
|
|
838
|
+
return { lines: [], errorLine: 0 };
|
|
839
|
+
// Line numbers and current-line markers sit in the first <td id="sourcetablecolumn">.
|
|
840
|
+
const columnMatches = Array.from(sourceHtml.matchAll(/<td[^>]*id="sourcetablecolumn"[^>]*>([\s\S]*?)<\/td>/gi));
|
|
841
|
+
const numberHtml = columnMatches[0]?.[1] ?? '';
|
|
842
|
+
const lineNumberMatches = Array.from(numberHtml.matchAll(/<span[^>]*class="linenumber[^"]*"[^>]*>([\s\S]*?)<\/span>/gi));
|
|
843
|
+
const numbers = lineNumberMatches.map((m) => {
|
|
844
|
+
const value = stripHtmlTags(m[1] ?? '').trim();
|
|
845
|
+
return /^\d+$/.test(value) ? Number(value) : null;
|
|
846
|
+
});
|
|
847
|
+
// Line source cells sit in the second <td id="sourcetablecolumn">.
|
|
848
|
+
const sourceCellHtml = columnMatches[1]?.[1] ?? '';
|
|
849
|
+
const lineDivs = Array.from(sourceCellHtml.matchAll(/<div[^>]*class="sourceline([^"]*)"[^>]*>([\s\S]*?)<\/div>/gi));
|
|
850
|
+
const lines = [];
|
|
851
|
+
let errorLine = 0;
|
|
852
|
+
let fallback = 1;
|
|
853
|
+
for (let i = 0; i < lineDivs.length; i++) {
|
|
854
|
+
const match = lineDivs[i];
|
|
855
|
+
const classes = (match[1] ?? '').trim();
|
|
856
|
+
const isError = /\bhighlight\b/i.test(classes);
|
|
857
|
+
const raw = stripHtmlTags(match[2] ?? '');
|
|
858
|
+
const content = decodeHtmlEntities(raw).replace(/\s+$/, '');
|
|
859
|
+
const assignedNumber = numbers[i];
|
|
860
|
+
const resolvedNumber = typeof assignedNumber === 'number' && assignedNumber > 0 ? assignedNumber : fallback;
|
|
861
|
+
fallback = resolvedNumber + 1;
|
|
862
|
+
lines.push({ number: resolvedNumber, content, isError });
|
|
863
|
+
if (isError && errorLine === 0)
|
|
864
|
+
errorLine = resolvedNumber;
|
|
865
|
+
}
|
|
866
|
+
return { lines, errorLine };
|
|
867
|
+
}
|
|
868
|
+
function extractGatewayCallStackFromHtml(stackHtml) {
|
|
869
|
+
if (!stackHtml)
|
|
870
|
+
return [];
|
|
871
|
+
const tableMatch = stackHtml.match(/<table[\s\S]*?<\/table>/i);
|
|
872
|
+
if (!tableMatch)
|
|
873
|
+
return [];
|
|
874
|
+
const tableHtml = tableMatch[0];
|
|
875
|
+
const rowMatches = Array.from(tableHtml.matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/gi));
|
|
876
|
+
const entries = [];
|
|
877
|
+
for (const row of rowMatches) {
|
|
878
|
+
const cells = Array.from(row[1].matchAll(/<td[^>]*>([\s\S]*?)<\/td>/gi)).map((m) => m[1] ?? '');
|
|
879
|
+
if (cells.length < 5)
|
|
880
|
+
continue;
|
|
881
|
+
const numberValue = Number(stripHtmlTags(cells[0]).replace(/\D+/g, '').trim());
|
|
882
|
+
if (!Number.isFinite(numberValue) || numberValue <= 0)
|
|
883
|
+
continue;
|
|
884
|
+
entries.push({
|
|
885
|
+
number: numberValue,
|
|
886
|
+
event: decodeHtmlEntities(stripHtmlTags(cells[1])).trim(),
|
|
887
|
+
program: decodeHtmlEntities(stripHtmlTags(cells[2])).trim(),
|
|
888
|
+
name: decodeHtmlEntities(stripHtmlTags(cells[3])).trim(),
|
|
889
|
+
line: safePositiveInt(stripHtmlTags(cells[4]).replace(/\D+/g, '')),
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
return entries;
|
|
893
|
+
}
|
|
894
|
+
function sanitizeHtmlCellValue(raw) {
|
|
895
|
+
let value = stripHtmlTags(raw);
|
|
896
|
+
value = decodeHtmlEntities(value);
|
|
897
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
898
|
+
}
|
|
899
|
+
function stripHtmlTags(html) {
|
|
900
|
+
return String(html ?? '').replace(/<[^>]*>/g, '');
|
|
901
|
+
}
|
|
902
|
+
function decodeHtmlEntities(text) {
|
|
903
|
+
return String(text ?? '')
|
|
904
|
+
.replace(/ /gi, ' ')
|
|
905
|
+
.replace(/&/gi, '&')
|
|
906
|
+
.replace(/</gi, '<')
|
|
907
|
+
.replace(/>/gi, '>')
|
|
908
|
+
.replace(/"/gi, '"')
|
|
909
|
+
.replace(/'/gi, "'")
|
|
910
|
+
.replace(/–/gi, '–')
|
|
911
|
+
.replace(/—/gi, '—')
|
|
912
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
|
|
913
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(parseInt(code, 16)));
|
|
914
|
+
}
|
|
915
|
+
function decodeUriComponentSafe(value) {
|
|
916
|
+
if (!value?.includes('%'))
|
|
917
|
+
return value;
|
|
918
|
+
try {
|
|
919
|
+
return decodeURIComponent(value);
|
|
920
|
+
}
|
|
921
|
+
catch {
|
|
922
|
+
return value;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function escapeRegex(value) {
|
|
926
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
927
|
+
}
|
|
270
928
|
//# sourceMappingURL=diagnostics.js.map
|