@sentry/api 0.423.0 → 0.424.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,6 +124,43 @@ Each `ApiOperation` records the actual exported `functionName`, original OpenAPI
124
124
 
125
125
  The catalog is generated from the same schema and SDK output as each release. Consumers own search, grouping, ranking, and command aliases; the catalog does not fetch a live schema or infer undocumented API behavior.
126
126
 
127
+ ### Build-time source analysis
128
+
129
+ `@sentry/api/source-analysis` follows references in supplied TypeScript source to discover calls to catalog operations. Install TypeScript only when using this optional build tool; the analyzer is tested with TypeScript 5.9. Its optional peer uses a wildcard so it does not constrain the compiler used by applications that only import the client.
130
+
131
+ ```bash
132
+ npm install --save-dev @sentry/api typescript@^5.9
133
+ ```
134
+
135
+ ```ts
136
+ import { operations } from "@sentry/api/operations";
137
+ import { analyzeSourceApiUsage } from "@sentry/api/source-analysis";
138
+
139
+ const sources = new Map([
140
+ ["features/logs.ts", `
141
+ import { listOrganizationEvents } from "@sentry/api";
142
+ export const readLogs = () =>
143
+ listOrganizationEvents({
144
+ path: { organization_id_or_slug: "example-org" },
145
+ query: { dataset: "logs", field: ["message"] },
146
+ });
147
+ `],
148
+ ]);
149
+ const analysis = analyzeSourceApiUsage(sources, operations, {
150
+ queryParameters: ["dataset"],
151
+ });
152
+ console.log(analysis.exports.get("features/logs.ts")?.get("readLogs")?.operations);
153
+ // Includes listOrganizationEvents with known query context { dataset: "logs" }.
154
+ ```
155
+
156
+ The analyzer parses source without executing it, following local imports, re-exports, aliases, and references. Supply sources together with the operation catalog from the installed SDK version. Run it during the consumer's build: command-to-operation relationships depend on consumer code and cannot be precomputed during SDK packaging.
157
+
158
+ Named, namespace, and default imports are supported, along with top-level functions, variable initializers, and default function/expression exports. Relative imports resolve only against the supplied sources. Extensionless paths try the exact file, `.ts`, `.tsx`, `index.ts`, then `index.tsx`; `.js` specifiers try `.ts` and `.tsx` before the exact `.js` file. Explicit `.ts`/`.tsx` specifiers use the exact file. Named re-exports can forward a default; `export *` forwards only named exports.
159
+
160
+ Consumers configure their own `boundaries` (factory names whose arguments are inspected without following the factory implementation), `requests` (raw-request wrapper argument positions), `queryParameters` (query values to retain), and `declarationFlags` (boolean markers). These options default to empty lists. Returned `boundaries` identify factory-created exports and their markers; `methods` records reachable HTTP methods, including known methods on unresolved raw requests. CLI routing, aliases, presentation, and destructive-command policy remain with the consumer.
161
+
162
+ This is static discovery, not proof of runtime execution or safety. Class/member dispatch, dynamic imports, CommonJS, and `tsconfig` path aliases are outside its scope. Missing sources and unsupported constructs can omit calls, so an empty result does not prove that no API calls exist. `unresolvedRequests` contains diagnostics for raw requests that cannot be resolved against the public catalog, not source-coverage diagnostics. Unknown query values are omitted, mutable `let` and `var` bindings are left unknown, and a related operation does not describe every request a workflow makes. The entry imports TypeScript separately and is not loaded by the root client, browser helpers, or operation catalog.
163
+
127
164
  ### Browser sessions
128
165
 
129
166
  The browser helper configures session cookies and CSRF token injection:
@@ -0,0 +1,75 @@
1
+ /** Build-time analysis of API usage in consumer-supplied TypeScript source. */
2
+ import type { ApiOperation } from "./operation-types.js";
3
+ /** Identity supplied by the generated SDK operation catalog. */
4
+ export type SourceApiOperation = Pick<ApiOperation, "functionName" | "method" | "path">;
5
+ /** A consumer's raw request wrapper, identified by its resolved function name. */
6
+ export type SourceApiRequest = {
7
+ functionName: string;
8
+ /** Zero-based argument containing the request path. */
9
+ pathArgument: number;
10
+ /** Zero-based argument containing the options object. */
11
+ optionsArgument: number;
12
+ /** Options property containing query parameters. Defaults to `query`. */
13
+ queryProperty?: string;
14
+ /** Method when the options omit it. Defaults to GET; unknown spreads stay unknown. */
15
+ defaultMethod?: string;
16
+ };
17
+ /** Consumer conventions; none of these lists are inferred from command names. */
18
+ export type SourceApiAnalysisOptions = {
19
+ /** Factory names whose returned objects contain deferred callbacks. */
20
+ boundaries?: readonly string[];
21
+ /** Raw request wrappers to link to the operation catalog. */
22
+ requests?: readonly SourceApiRequest[];
23
+ /** Query keys to retain. The Sentry `query` key also retains fixed joined terms. */
24
+ queryParameters?: readonly string[];
25
+ /** Boolean property names to report when explicitly set to true. */
26
+ declarationFlags?: readonly string[];
27
+ };
28
+ /** An operation reachable from a source declaration. */
29
+ export type SourceOperationUsage = SourceApiOperation & {
30
+ /** Known query values or fixed terms; callers may add further filters. */
31
+ query?: Record<string, string>;
32
+ };
33
+ /** A raw request whose identity cannot be established from the public catalog. */
34
+ export type UnresolvedApiRequest = {
35
+ /** Source module, using the caller's source-map keys. */
36
+ file: string;
37
+ /** One-based request call line. */
38
+ line: number;
39
+ /** Known HTTP method, when statically available. */
40
+ method?: string;
41
+ /** Known path template, when statically available. */
42
+ path?: string;
43
+ /** Why no operation was selected. */
44
+ reason: string;
45
+ };
46
+ /** Source-derived relationships; no command is executed to obtain them. */
47
+ export type SourceApiUsage = {
48
+ /** Discovered API operations, preserving distinct contexts. */
49
+ operations: SourceOperationUsage[];
50
+ /** Reachable raw requests that need explicit discovery diagnostics. */
51
+ unresolvedRequests: UnresolvedApiRequest[];
52
+ /** Known HTTP methods, including unresolved requests and static method properties. */
53
+ methods: string[];
54
+ };
55
+ /** Source relationships and deferred callback boundaries; never executes input code. */
56
+ export type SourceApiAnalysis = {
57
+ /** Source file → exported symbol → reachable API usage. */
58
+ exports: Map<string, Map<string, SourceApiUsage>>;
59
+ /** Exported boundaries and their consumer-selected factory/flag evidence. */
60
+ boundaries: {
61
+ file: string;
62
+ name: string;
63
+ factories: string[];
64
+ flags: string[];
65
+ }[];
66
+ };
67
+ /**
68
+ * Follow function references, imports and barrel exports to SDK calls or known
69
+ * raw request wrappers. Dynamic paths remain diagnostics rather than guesses.
70
+ * Boundary factories have their arguments inspected, without traversing the
71
+ * factory's execution machinery. Reading their returned objects as data does
72
+ * not inherit callback usage. Results describe static reachability, not a
73
+ * complete execution trace; dynamic behavior may not be discoverable.
74
+ */
75
+ export declare function analyzeSourceApiUsage(sources: ReadonlyMap<string, string>, operations: readonly SourceApiOperation[], options?: SourceApiAnalysisOptions): SourceApiAnalysis;
@@ -0,0 +1,796 @@
1
+ // node:path
2
+ function assertPath(path) {
3
+ if (typeof path !== "string")
4
+ throw TypeError("Path must be a string. Received " + JSON.stringify(path));
5
+ }
6
+ function normalizeStringPosix(path, allowAboveRoot) {
7
+ var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
8
+ for (var i = 0;i <= path.length; ++i) {
9
+ if (i < path.length)
10
+ code = path.charCodeAt(i);
11
+ else if (code === 47)
12
+ break;
13
+ else
14
+ code = 47;
15
+ if (code === 47) {
16
+ if (lastSlash === i - 1 || dots === 1)
17
+ ;
18
+ else if (lastSlash !== i - 1 && dots === 2) {
19
+ if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
20
+ if (res.length > 2) {
21
+ var lastSlashIndex = res.lastIndexOf("/");
22
+ if (lastSlashIndex !== res.length - 1) {
23
+ if (lastSlashIndex === -1)
24
+ res = "", lastSegmentLength = 0;
25
+ else
26
+ res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
27
+ lastSlash = i, dots = 0;
28
+ continue;
29
+ }
30
+ } else if (res.length === 2 || res.length === 1) {
31
+ res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
32
+ continue;
33
+ }
34
+ }
35
+ if (allowAboveRoot) {
36
+ if (res.length > 0)
37
+ res += "/..";
38
+ else
39
+ res = "..";
40
+ lastSegmentLength = 2;
41
+ }
42
+ } else {
43
+ if (res.length > 0)
44
+ res += "/" + path.slice(lastSlash + 1, i);
45
+ else
46
+ res = path.slice(lastSlash + 1, i);
47
+ lastSegmentLength = i - lastSlash - 1;
48
+ }
49
+ lastSlash = i, dots = 0;
50
+ } else if (code === 46 && dots !== -1)
51
+ ++dots;
52
+ else
53
+ dots = -1;
54
+ }
55
+ return res;
56
+ }
57
+ function _format(sep, pathObject) {
58
+ var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
59
+ if (!dir)
60
+ return base;
61
+ if (dir === pathObject.root)
62
+ return dir + base;
63
+ return dir + sep + base;
64
+ }
65
+ function resolve() {
66
+ var resolvedPath = "", resolvedAbsolute = false, cwd;
67
+ for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) {
68
+ var path;
69
+ if (i >= 0)
70
+ path = arguments[i];
71
+ else {
72
+ if (cwd === undefined)
73
+ cwd = process.cwd();
74
+ path = cwd;
75
+ }
76
+ if (assertPath(path), path.length === 0)
77
+ continue;
78
+ resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
79
+ }
80
+ if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
81
+ if (resolvedPath.length > 0)
82
+ return "/" + resolvedPath;
83
+ else
84
+ return "/";
85
+ else if (resolvedPath.length > 0)
86
+ return resolvedPath;
87
+ else
88
+ return ".";
89
+ }
90
+ function normalize(path) {
91
+ if (assertPath(path), path.length === 0)
92
+ return ".";
93
+ var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
94
+ if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute)
95
+ path = ".";
96
+ if (path.length > 0 && trailingSeparator)
97
+ path += "/";
98
+ if (isAbsolute)
99
+ return "/" + path;
100
+ return path;
101
+ }
102
+ function isAbsolute(path) {
103
+ return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
104
+ }
105
+ function join() {
106
+ if (arguments.length === 0)
107
+ return ".";
108
+ var joined;
109
+ for (var i = 0;i < arguments.length; ++i) {
110
+ var arg = arguments[i];
111
+ if (assertPath(arg), arg.length > 0)
112
+ if (joined === undefined)
113
+ joined = arg;
114
+ else
115
+ joined += "/" + arg;
116
+ }
117
+ if (joined === undefined)
118
+ return ".";
119
+ return normalize(joined);
120
+ }
121
+ function relative(from, to) {
122
+ if (assertPath(from), assertPath(to), from === to)
123
+ return "";
124
+ if (from = resolve(from), to = resolve(to), from === to)
125
+ return "";
126
+ var fromStart = 1;
127
+ for (;fromStart < from.length; ++fromStart)
128
+ if (from.charCodeAt(fromStart) !== 47)
129
+ break;
130
+ var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
131
+ for (;toStart < to.length; ++toStart)
132
+ if (to.charCodeAt(toStart) !== 47)
133
+ break;
134
+ var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
135
+ for (;i <= length; ++i) {
136
+ if (i === length) {
137
+ if (toLen > length) {
138
+ if (to.charCodeAt(toStart + i) === 47)
139
+ return to.slice(toStart + i + 1);
140
+ else if (i === 0)
141
+ return to.slice(toStart + i);
142
+ } else if (fromLen > length) {
143
+ if (from.charCodeAt(fromStart + i) === 47)
144
+ lastCommonSep = i;
145
+ else if (i === 0)
146
+ lastCommonSep = 0;
147
+ }
148
+ break;
149
+ }
150
+ var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
151
+ if (fromCode !== toCode)
152
+ break;
153
+ else if (fromCode === 47)
154
+ lastCommonSep = i;
155
+ }
156
+ var out = "";
157
+ for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i)
158
+ if (i === fromEnd || from.charCodeAt(i) === 47)
159
+ if (out.length === 0)
160
+ out += "..";
161
+ else
162
+ out += "/..";
163
+ if (out.length > 0)
164
+ return out + to.slice(toStart + lastCommonSep);
165
+ else {
166
+ if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47)
167
+ ++toStart;
168
+ return to.slice(toStart);
169
+ }
170
+ }
171
+ function _makeLong(path) {
172
+ return path;
173
+ }
174
+ function dirname(path) {
175
+ if (assertPath(path), path.length === 0)
176
+ return ".";
177
+ var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
178
+ for (var i = path.length - 1;i >= 1; --i)
179
+ if (code = path.charCodeAt(i), code === 47) {
180
+ if (!matchedSlash) {
181
+ end = i;
182
+ break;
183
+ }
184
+ } else
185
+ matchedSlash = false;
186
+ if (end === -1)
187
+ return hasRoot ? "/" : ".";
188
+ if (hasRoot && end === 1)
189
+ return "//";
190
+ return path.slice(0, end);
191
+ }
192
+ function basename(path, ext) {
193
+ if (ext !== undefined && typeof ext !== "string")
194
+ throw TypeError('"ext" argument must be a string');
195
+ assertPath(path);
196
+ var start = 0, end = -1, matchedSlash = true, i;
197
+ if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
198
+ if (ext.length === path.length && ext === path)
199
+ return "";
200
+ var extIdx = ext.length - 1, firstNonSlashEnd = -1;
201
+ for (i = path.length - 1;i >= 0; --i) {
202
+ var code = path.charCodeAt(i);
203
+ if (code === 47) {
204
+ if (!matchedSlash) {
205
+ start = i + 1;
206
+ break;
207
+ }
208
+ } else {
209
+ if (firstNonSlashEnd === -1)
210
+ matchedSlash = false, firstNonSlashEnd = i + 1;
211
+ if (extIdx >= 0)
212
+ if (code === ext.charCodeAt(extIdx)) {
213
+ if (--extIdx === -1)
214
+ end = i;
215
+ } else
216
+ extIdx = -1, end = firstNonSlashEnd;
217
+ }
218
+ }
219
+ if (start === end)
220
+ end = firstNonSlashEnd;
221
+ else if (end === -1)
222
+ end = path.length;
223
+ return path.slice(start, end);
224
+ } else {
225
+ for (i = path.length - 1;i >= 0; --i)
226
+ if (path.charCodeAt(i) === 47) {
227
+ if (!matchedSlash) {
228
+ start = i + 1;
229
+ break;
230
+ }
231
+ } else if (end === -1)
232
+ matchedSlash = false, end = i + 1;
233
+ if (end === -1)
234
+ return "";
235
+ return path.slice(start, end);
236
+ }
237
+ }
238
+ function extname(path) {
239
+ assertPath(path);
240
+ var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
241
+ for (var i = path.length - 1;i >= 0; --i) {
242
+ var code = path.charCodeAt(i);
243
+ if (code === 47) {
244
+ if (!matchedSlash) {
245
+ startPart = i + 1;
246
+ break;
247
+ }
248
+ continue;
249
+ }
250
+ if (end === -1)
251
+ matchedSlash = false, end = i + 1;
252
+ if (code === 46) {
253
+ if (startDot === -1)
254
+ startDot = i;
255
+ else if (preDotState !== 1)
256
+ preDotState = 1;
257
+ } else if (startDot !== -1)
258
+ preDotState = -1;
259
+ }
260
+ if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
261
+ return "";
262
+ return path.slice(startDot, end);
263
+ }
264
+ function format(pathObject) {
265
+ if (pathObject === null || typeof pathObject !== "object")
266
+ throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
267
+ return _format("/", pathObject);
268
+ }
269
+ function parse(path) {
270
+ assertPath(path);
271
+ var ret = { root: "", dir: "", base: "", ext: "", name: "" };
272
+ if (path.length === 0)
273
+ return ret;
274
+ var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
275
+ if (isAbsolute2)
276
+ ret.root = "/", start = 1;
277
+ else
278
+ start = 0;
279
+ var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
280
+ for (;i >= start; --i) {
281
+ if (code = path.charCodeAt(i), code === 47) {
282
+ if (!matchedSlash) {
283
+ startPart = i + 1;
284
+ break;
285
+ }
286
+ continue;
287
+ }
288
+ if (end === -1)
289
+ matchedSlash = false, end = i + 1;
290
+ if (code === 46) {
291
+ if (startDot === -1)
292
+ startDot = i;
293
+ else if (preDotState !== 1)
294
+ preDotState = 1;
295
+ } else if (startDot !== -1)
296
+ preDotState = -1;
297
+ }
298
+ if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
299
+ if (end !== -1)
300
+ if (startPart === 0 && isAbsolute2)
301
+ ret.base = ret.name = path.slice(1, end);
302
+ else
303
+ ret.base = ret.name = path.slice(startPart, end);
304
+ } else {
305
+ if (startPart === 0 && isAbsolute2)
306
+ ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
307
+ else
308
+ ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
309
+ ret.ext = path.slice(startDot, end);
310
+ }
311
+ if (startPart > 0)
312
+ ret.dir = path.slice(0, startPart - 1);
313
+ else if (isAbsolute2)
314
+ ret.dir = "/";
315
+ return ret;
316
+ }
317
+ var sep = "/";
318
+ var delimiter = ":";
319
+ var posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
320
+
321
+ // src/source-analysis.ts
322
+ import ts from "typescript";
323
+ var SDK = "@sentry/api";
324
+ var API_PREFIX = /^\/api\/\d+(?=\/)/;
325
+ var PATH_PARAMETER = /\{[^}]*\}/g;
326
+ function unwrap(node) {
327
+ return ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node) || ts.isNonNullExpression(node) ? unwrap(node.expression) : node;
328
+ }
329
+ function bindsName(binding, name) {
330
+ return ts.isIdentifier(binding) ? binding.text === name : binding.elements.some((element) => ts.isBindingElement(element) && bindsName(element.name, name));
331
+ }
332
+ function lexicalDeclaration(node, includeSource = false) {
333
+ for (let parent = node.parent;parent && (includeSource || !ts.isSourceFile(parent)); parent = parent.parent) {
334
+ if (ts.isFunctionLike(parent)) {
335
+ const parameter = parent.parameters.find((parameter2) => bindsName(parameter2.name, node.text));
336
+ if (parameter)
337
+ return parameter;
338
+ }
339
+ if ((ts.isFunctionExpression(parent) || ts.isClassExpression(parent)) && parent.name?.text === node.text)
340
+ return parent;
341
+ if (ts.isCatchClause(parent) && parent.variableDeclaration && bindsName(parent.variableDeclaration.name, node.text))
342
+ return parent.variableDeclaration;
343
+ if ((ts.isForStatement(parent) || ts.isForOfStatement(parent) || ts.isForInStatement(parent)) && parent.initializer && ts.isVariableDeclarationList(parent.initializer)) {
344
+ const declaration = parent.initializer.declarations.find((declaration2) => bindsName(declaration2.name, node.text));
345
+ if (declaration)
346
+ return declaration;
347
+ }
348
+ if (ts.isBlock(parent) || ts.isSourceFile(parent)) {
349
+ for (const statement of parent.statements) {
350
+ if (ts.isVariableStatement(statement)) {
351
+ const declaration = statement.declarationList.declarations.find((declaration2) => bindsName(declaration2.name, node.text));
352
+ if (declaration)
353
+ return declaration;
354
+ }
355
+ if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name?.text === node.text)
356
+ return statement;
357
+ }
358
+ }
359
+ }
360
+ return;
361
+ }
362
+ function sourceModule(file, specifier, sources) {
363
+ if (!specifier.startsWith("."))
364
+ return specifier;
365
+ const target = posix.normalize(posix.join(dirname(file), specifier));
366
+ const extension = posix.extname(target);
367
+ const candidates = extension === ".js" ? [target.replace(/\.js$/, ".ts"), target.replace(/\.js$/, ".tsx"), target] : extension ? [target] : [target, `${target}.ts`, `${target}.tsx`, posix.join(target, "index.ts"), posix.join(target, "index.tsx")];
368
+ return candidates.find((candidate) => sources.has(candidate)) ?? target;
369
+ }
370
+ function parseModule(file, text, sources) {
371
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
372
+ const mod = {
373
+ source,
374
+ imports: new Map,
375
+ exports: new Map,
376
+ stars: [],
377
+ declarations: new Map
378
+ };
379
+ for (const statement of source.statements) {
380
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
381
+ if (statement.importClause?.isTypeOnly)
382
+ continue;
383
+ const target = sourceModule(file, statement.moduleSpecifier.text, sources);
384
+ const defaultImport = statement.importClause?.name;
385
+ if (defaultImport)
386
+ mod.imports.set(defaultImport.text, { file: target, name: "default" });
387
+ const bindings = statement.importClause?.namedBindings;
388
+ if (bindings && ts.isNamedImports(bindings)) {
389
+ for (const binding of bindings.elements) {
390
+ if (!binding.isTypeOnly)
391
+ mod.imports.set(binding.name.text, {
392
+ file: target,
393
+ name: (binding.propertyName ?? binding.name).text
394
+ });
395
+ }
396
+ } else if (bindings && ts.isNamespaceImport(bindings)) {
397
+ mod.imports.set(bindings.name.text, { file: target, name: "*" });
398
+ }
399
+ continue;
400
+ }
401
+ if (ts.isExportDeclaration(statement) && !statement.isTypeOnly) {
402
+ const target = statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) ? sourceModule(file, statement.moduleSpecifier.text, sources) : file;
403
+ if (!statement.exportClause)
404
+ mod.stars.push(target);
405
+ else if (ts.isNamedExports(statement.exportClause)) {
406
+ for (const binding of statement.exportClause.elements) {
407
+ if (!binding.isTypeOnly)
408
+ mod.exports.set(binding.name.text, {
409
+ file: target,
410
+ name: (binding.propertyName ?? binding.name).text
411
+ });
412
+ }
413
+ }
414
+ continue;
415
+ }
416
+ const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
417
+ const isDefault = modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) || ts.isExportAssignment(statement) && !statement.isExportEquals;
418
+ const nodes = [];
419
+ if (ts.isFunctionDeclaration(statement) && statement.body && (statement.name || isDefault))
420
+ nodes.push([statement.name?.text ?? "default", statement]);
421
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals)
422
+ nodes.push(["default", statement.expression]);
423
+ if (ts.isVariableStatement(statement)) {
424
+ for (const declaration of statement.declarationList.declarations) {
425
+ if (ts.isIdentifier(declaration.name) && declaration.initializer)
426
+ nodes.push([declaration.name.text, declaration.initializer]);
427
+ }
428
+ }
429
+ for (const [name, node] of nodes) {
430
+ mod.declarations.set(name, {
431
+ node,
432
+ refs: new Set,
433
+ calls: new Set,
434
+ usage: { operations: [], unresolvedRequests: [], methods: [] },
435
+ boundary: false,
436
+ factories: new Set,
437
+ flags: new Set
438
+ });
439
+ if (isDefault)
440
+ mod.exports.set("default", { file, name });
441
+ else if (modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword))
442
+ mod.exports.set(name, { file, name });
443
+ }
444
+ }
445
+ return mod;
446
+ }
447
+ function analyzeSourceApiUsage(sources, operations, options = {}) {
448
+ const boundaries = new Set(options.boundaries);
449
+ const requests = new Map(options.requests?.map((request) => [request.functionName, request]));
450
+ const declarationFlags = new Set(options.declarationFlags);
451
+ const modules = new Map([...sources].map(([file, text]) => [
452
+ file,
453
+ parseModule(file, text, sources)
454
+ ]));
455
+ const sdk = new Map(operations.map((operation) => [operation.functionName, operation]));
456
+ const identities = new Map;
457
+ const pathKey = (path) => `/${path.replace(API_PREFIX, "").split("/").filter(Boolean).join("/")}/`.replace(PATH_PARAMETER, "{}");
458
+ for (const operation of operations) {
459
+ const key = `${operation.method}:${pathKey(operation.path)}`;
460
+ identities.set(key, [...identities.get(key) ?? [], operation]);
461
+ }
462
+ function resolve2(binding, exported = false, seen = new Set) {
463
+ if (binding.file === SDK)
464
+ return binding;
465
+ const key = `${binding.file}#${binding.name}#${exported}`;
466
+ if (seen.has(key))
467
+ return;
468
+ seen.add(key);
469
+ const mod = modules.get(binding.file);
470
+ if (!mod)
471
+ return;
472
+ if (!exported) {
473
+ if (mod.declarations.has(binding.name))
474
+ return binding;
475
+ const imported = mod.imports.get(binding.name);
476
+ return imported ? resolve2(imported, true, seen) : undefined;
477
+ }
478
+ const next = mod.exports.get(binding.name);
479
+ if (next)
480
+ return resolve2(next, next.file !== binding.file, seen);
481
+ if (binding.name === "default")
482
+ return;
483
+ for (const target of mod.stars) {
484
+ const found = resolve2({ file: target, name: binding.name }, true, seen);
485
+ if (found)
486
+ return found;
487
+ }
488
+ return;
489
+ }
490
+ function bindingFor(file, node) {
491
+ const mod = modules.get(file);
492
+ if (ts.isIdentifier(node)) {
493
+ if (lexicalDeclaration(node))
494
+ return;
495
+ const imported = mod.imports.get(node.text);
496
+ return imported ? resolve2(imported, true) ?? (!modules.has(imported.file) ? imported : undefined) : resolve2({ file, name: node.text });
497
+ }
498
+ if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && !lexicalDeclaration(node.expression)) {
499
+ const namespace = mod.imports.get(node.expression.text);
500
+ if (namespace?.name === "*")
501
+ return resolve2({ file: namespace.file, name: node.name.text }, true);
502
+ }
503
+ return;
504
+ }
505
+ function callableBinding(binding) {
506
+ const seen = new Set;
507
+ while (binding?.name === "default") {
508
+ const key = `${binding.file}#${binding.name}`;
509
+ if (seen.has(key))
510
+ break;
511
+ seen.add(key);
512
+ const declaration = modules.get(binding.file)?.declarations.get(binding.name);
513
+ if (!declaration)
514
+ break;
515
+ const node = unwrap(declaration.node);
516
+ if (!ts.isIdentifier(node))
517
+ break;
518
+ const target = bindingFor(binding.file, node);
519
+ if (!target)
520
+ break;
521
+ binding = target;
522
+ }
523
+ return binding;
524
+ }
525
+ function initializer(node) {
526
+ if (!ts.isIdentifier(node))
527
+ return;
528
+ const declaration = lexicalDeclaration(node, true);
529
+ return declaration && ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && ts.isVariableDeclarationList(declaration.parent) && declaration.parent.flags & ts.NodeFlags.Const ? declaration.initializer : undefined;
530
+ }
531
+ function value(node, template = false, seen = new Set) {
532
+ if (!node || seen.has(node))
533
+ return;
534
+ seen.add(node);
535
+ node = unwrap(node);
536
+ if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node))
537
+ return node.text;
538
+ if (node.kind === ts.SyntaxKind.TrueKeyword)
539
+ return "true";
540
+ if (node.kind === ts.SyntaxKind.FalseKeyword)
541
+ return "false";
542
+ if (ts.isIdentifier(node))
543
+ return value(initializer(node), template, seen);
544
+ if (ts.isTemplateExpression(node)) {
545
+ let result2 = node.head.text;
546
+ for (const span of node.templateSpans) {
547
+ const part = value(span.expression, false, new Set(seen));
548
+ if (part === undefined && !template)
549
+ return;
550
+ result2 += (part ?? "{}") + span.literal.text;
551
+ }
552
+ return result2;
553
+ }
554
+ return;
555
+ }
556
+ function property(node, name, seen = new Set) {
557
+ if (node == null)
558
+ return node;
559
+ if (seen.has(node))
560
+ return null;
561
+ seen.add(node);
562
+ node = unwrap(node);
563
+ if (ts.isIdentifier(node)) {
564
+ const initial = initializer(node);
565
+ return initial ? property(initial, name, seen) : null;
566
+ }
567
+ if (!ts.isObjectLiteralExpression(node))
568
+ return null;
569
+ for (const member of [...node.properties].reverse()) {
570
+ if (ts.isSpreadAssignment(member)) {
571
+ const found = property(member.expression, name, new Set(seen));
572
+ if (found !== undefined)
573
+ return found;
574
+ continue;
575
+ }
576
+ const key = ts.isComputedPropertyName(member.name) ? value(member.name.expression) : member.name.text;
577
+ if (key === undefined)
578
+ return null;
579
+ if (key !== name)
580
+ continue;
581
+ if (ts.isPropertyAssignment(member))
582
+ return member.initializer;
583
+ if (ts.isShorthandPropertyAssignment(member))
584
+ return member.name;
585
+ return null;
586
+ }
587
+ return;
588
+ }
589
+ function queryTerms(node, seen = new Set) {
590
+ if (!node || seen.has(node))
591
+ return;
592
+ const exact = value(node);
593
+ if (exact !== undefined)
594
+ return exact;
595
+ seen.add(node);
596
+ node = unwrap(node);
597
+ if (ts.isIdentifier(node))
598
+ return queryTerms(initializer(node), seen);
599
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.BarBarToken)
600
+ return queryTerms(node.left, seen);
601
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "join" || value(node.arguments[0]) !== " ")
602
+ return;
603
+ let array = node.expression.expression;
604
+ if (ts.isCallExpression(array) && ts.isPropertyAccessExpression(array.expression) && array.expression.name.text === "filter" && array.arguments[0]?.getText() === "Boolean")
605
+ array = array.expression.expression;
606
+ if (!ts.isArrayLiteralExpression(array))
607
+ return;
608
+ const terms = array.elements.map((item) => value(item)).filter((term) => !!term);
609
+ return terms.length ? terms.join(" ") : undefined;
610
+ }
611
+ function queryContext(requestOptions, key) {
612
+ const query = property(requestOptions, key);
613
+ const result2 = {};
614
+ for (const name of options.queryParameters ?? []) {
615
+ const expression = property(query, name);
616
+ const text = name === "query" ? queryTerms(expression) : value(expression);
617
+ if (text)
618
+ result2[name] = text;
619
+ }
620
+ return Object.keys(result2).length ? result2 : undefined;
621
+ }
622
+ for (const [file, mod] of modules) {
623
+ for (const declaration of mod.declarations.values()) {
624
+ let visit = function(node) {
625
+ if (ts.isTypeNode(node))
626
+ return;
627
+ if (ts.isPropertyAssignment(node) && declarationFlags.has(node.name.getText()) && node.initializer.kind === ts.SyntaxKind.TrueKeyword && property(node.parent, node.name.getText()) === node.initializer)
628
+ declaration.flags.add(node.name.getText());
629
+ if (ts.isPropertyAssignment(node) && node.name.getText() === "method") {
630
+ const method = value(node.initializer);
631
+ if (method)
632
+ declaration.usage.methods.push(method.toUpperCase());
633
+ }
634
+ if (ts.isCallExpression(node)) {
635
+ const reference = bindingFor(file, node.expression);
636
+ if (reference)
637
+ declaration.calls.add(`${reference.file}#${reference.name}`);
638
+ const binding = callableBinding(reference);
639
+ const name = binding?.name ?? (ts.isIdentifier(node.expression) && !lexicalDeclaration(node.expression) ? node.expression.text : "");
640
+ if (boundaries.has(name)) {
641
+ declaration.boundary = true;
642
+ declaration.factories.add(name);
643
+ for (const argument of node.arguments)
644
+ visit(argument);
645
+ return;
646
+ }
647
+ const operation = binding?.file === SDK ? sdk.get(name) : undefined;
648
+ if (operation) {
649
+ declaration.usage.operations.push({
650
+ ...operation,
651
+ query: queryContext(node.arguments[0], "query")
652
+ });
653
+ declaration.usage.methods.push(operation.method);
654
+ for (const argument of node.arguments)
655
+ visit(argument);
656
+ return;
657
+ }
658
+ const request = requests.get(name);
659
+ if (request) {
660
+ const requestOptions = node.arguments[request.optionsArgument];
661
+ const methodExpression = property(requestOptions, "method");
662
+ const method = methodExpression === undefined ? (request.defaultMethod ?? "GET").toUpperCase() : value(methodExpression)?.toUpperCase();
663
+ const path = value(node.arguments[request.pathArgument], true);
664
+ const matches = method && path ? identities.get(`${method}:${pathKey(path)}`) ?? [] : [];
665
+ if (method)
666
+ declaration.usage.methods.push(method);
667
+ if (matches.length === 1)
668
+ declaration.usage.operations.push({
669
+ ...matches[0],
670
+ query: queryContext(requestOptions, request.queryProperty ?? "query")
671
+ });
672
+ else
673
+ declaration.usage.unresolvedRequests.push({
674
+ file,
675
+ line: mod.source.getLineAndCharacterOfPosition(node.getStart()).line + 1,
676
+ method,
677
+ path,
678
+ reason: !method || !path ? "Dynamic request identity" : matches.length ? "Ambiguous catalog path" : "Request is not in the public catalog"
679
+ });
680
+ for (const argument of node.arguments)
681
+ visit(argument);
682
+ return;
683
+ }
684
+ }
685
+ if (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)) {
686
+ const parent = node.parent;
687
+ const isName = ts.isPropertyAccessExpression(parent) && parent.name === node || (ts.isPropertyAssignment(parent) || ts.isMethodDeclaration(parent) || ts.isVariableDeclaration(parent) || ts.isParameter(parent) || ts.isFunctionDeclaration(parent)) && parent.name === node;
688
+ if (!isName) {
689
+ const binding = bindingFor(file, node);
690
+ if (binding && !boundaries.has(binding.name)) {
691
+ const operation = binding.file === SDK ? sdk.get(binding.name) : undefined;
692
+ if (operation) {
693
+ declaration.usage.operations.push(operation);
694
+ declaration.usage.methods.push(operation.method);
695
+ } else
696
+ declaration.refs.add(`${binding.file}#${binding.name}`);
697
+ }
698
+ }
699
+ }
700
+ node.forEachChild(visit);
701
+ };
702
+ visit(declaration.node);
703
+ }
704
+ }
705
+ const declarations = new Map([...modules].flatMap(([file, mod]) => [...mod.declarations].map(([name, declaration]) => [`${file}#${name}`, declaration])));
706
+ function unique(items) {
707
+ return [
708
+ ...new Map(items.map((item) => [JSON.stringify(item), item])).values()
709
+ ];
710
+ }
711
+ function isAlias(declaration, ref) {
712
+ const node = unwrap(declaration.node);
713
+ if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node))
714
+ return false;
715
+ const binding = bindingFor(node.getSourceFile().fileName, node);
716
+ return !!binding && `${binding.file}#${binding.name}` === ref;
717
+ }
718
+ let changed = true;
719
+ while (changed) {
720
+ changed = false;
721
+ for (const declaration of declarations.values()) {
722
+ for (const ref of declaration.refs) {
723
+ const child = declarations.get(ref);
724
+ if (!child?.boundary || !declaration.calls.has(ref) && !isAlias(declaration, ref))
725
+ continue;
726
+ if (!declaration.boundary)
727
+ changed = true;
728
+ declaration.boundary = true;
729
+ for (const factory of child.factories) {
730
+ if (!declaration.factories.has(factory))
731
+ changed = true;
732
+ declaration.factories.add(factory);
733
+ }
734
+ for (const flag of child.flags) {
735
+ if (!declaration.flags.has(flag))
736
+ changed = true;
737
+ declaration.flags.add(flag);
738
+ }
739
+ }
740
+ }
741
+ }
742
+ changed = true;
743
+ while (changed) {
744
+ changed = false;
745
+ for (const declaration of declarations.values()) {
746
+ const before = JSON.stringify(declaration.usage);
747
+ for (const ref of declaration.refs) {
748
+ const child = declarations.get(ref);
749
+ if (!child)
750
+ continue;
751
+ if (child.boundary && !ts.isFunctionLike(unwrap(child.node)) && !isAlias(declaration, ref))
752
+ continue;
753
+ declaration.usage.operations.push(...child.usage.operations);
754
+ declaration.usage.unresolvedRequests.push(...child.usage.unresolvedRequests);
755
+ declaration.usage.methods.push(...child.usage.methods);
756
+ }
757
+ declaration.usage.operations = unique(declaration.usage.operations);
758
+ declaration.usage.unresolvedRequests = unique(declaration.usage.unresolvedRequests);
759
+ declaration.usage.methods = unique(declaration.usage.methods);
760
+ changed ||= before !== JSON.stringify(declaration.usage);
761
+ }
762
+ }
763
+ const result = { exports: new Map, boundaries: [] };
764
+ function exportNames(file, seen = new Set) {
765
+ if (seen.has(file))
766
+ return [];
767
+ seen.add(file);
768
+ const mod = modules.get(file);
769
+ return mod ? [
770
+ ...mod.exports.keys(),
771
+ ...mod.stars.flatMap((target) => exportNames(target, seen).filter((name) => name !== "default"))
772
+ ] : [];
773
+ }
774
+ for (const file of modules.keys()) {
775
+ const exported = new Map;
776
+ for (const name of new Set(exportNames(file))) {
777
+ const binding = resolve2({ file, name }, true);
778
+ const declaration = binding && declarations.get(`${binding.file}#${binding.name}`);
779
+ if (!declaration)
780
+ continue;
781
+ exported.set(name, declaration.usage);
782
+ if (declaration.boundary)
783
+ result.boundaries.push({
784
+ file,
785
+ name,
786
+ factories: [...declaration.factories],
787
+ flags: [...declaration.flags]
788
+ });
789
+ }
790
+ result.exports.set(file, exported);
791
+ }
792
+ return result;
793
+ }
794
+ export {
795
+ analyzeSourceApiUsage
796
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/api",
3
- "version": "0.423.0",
3
+ "version": "0.424.0",
4
4
  "description": "Official auto-generated TypeScript client for the Sentry public REST API",
5
5
  "keywords": [
6
6
  "sentry",
@@ -42,6 +42,10 @@
42
42
  "./operations": {
43
43
  "types": "./dist/operations.d.ts",
44
44
  "import": "./dist/operations.js"
45
+ },
46
+ "./source-analysis": {
47
+ "types": "./dist/source-analysis.d.ts",
48
+ "import": "./dist/source-analysis.js"
45
49
  }
46
50
  },
47
51
  "homepage": "https://docs.sentry.io/api/",
@@ -56,10 +60,14 @@
56
60
  "url": "https://github.com/getsentry/sentry-api-schema/issues"
57
61
  },
58
62
  "peerDependencies": {
63
+ "typescript": "*",
59
64
  "valibot": "*",
60
65
  "zod": "^3.24.0"
61
66
  },
62
67
  "peerDependenciesMeta": {
68
+ "typescript": {
69
+ "optional": true
70
+ },
63
71
  "valibot": {
64
72
  "optional": true
65
73
  },