@contrail/cli 3.5.0-alpha.dd20a19 → 3.5.1-alpha.63493a5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +3 -35
  2. package/dist/api/types.d.ts +0 -5
  3. package/dist/api/types.js +1 -36
  4. package/dist/commands/types/getAll.js +0 -18
  5. package/dist/commands/types/loadAll.js +0 -2
  6. package/dist/commands/types/mergeTypeProperties.js +0 -5
  7. package/dist/commands/types/uploadOptionSetHierarchies.js +0 -117
  8. package/dist/commands/types/uploadOptionSets.js +1 -1
  9. package/dist/commands/types/uploadRuleSets.js +2 -85
  10. package/dist/commands/types/uploadTypePolicies.js +2 -86
  11. package/dist/commands/types/uploadTypePropertyPolicies.d.ts +0 -1
  12. package/dist/commands/types/uploadTypePropertyPolicies.js +3 -139
  13. package/dist/commands/types/uploadUsageInfo.d.ts +3 -0
  14. package/dist/commands/types/uploadUsageInfo.js +4 -1
  15. package/dist/helpers/types/type-uploader.js +1 -1
  16. package/dist/templates/typescript/tsup.config.ts +0 -0
  17. package/node_modules/@contrail/loader/lib/color-processor.js +9 -23
  18. package/node_modules/@contrail/loader/lib/color-processor.spec.js +112 -62
  19. package/node_modules/@contrail/loader/lib/common/chunk-by-url-length.d.ts +1 -1
  20. package/node_modules/@contrail/loader/lib/common/chunk-by-url-length.js +2 -0
  21. package/node_modules/@contrail/loader/lib/common/chunk-by-url-length.spec.js +4 -0
  22. package/node_modules/@contrail/loader/lib/common/color-validation.d.ts +26 -0
  23. package/node_modules/@contrail/loader/lib/common/color-validation.js +163 -0
  24. package/node_modules/@contrail/loader/lib/common/color-validation.spec.js +102 -0
  25. package/node_modules/@contrail/loader/lib/common/size-range-helpers.js +0 -1
  26. package/node_modules/@contrail/loader/lib/constants.d.ts +0 -3
  27. package/node_modules/@contrail/loader/lib/constants.js +1 -4
  28. package/node_modules/@contrail/loader/lib/entities-v2/colors-v2/color-processor-v2.js +9 -23
  29. package/node_modules/@contrail/loader/lib/entities-v2/colors-v2/color-processor-v2.spec.js +112 -62
  30. package/node_modules/@contrail/loader/lib/entities-v2/entities-helper.js +0 -1
  31. package/node_modules/@contrail/loader/lib/entities-v2/linesheet-v2/assortment-processor.js +0 -1
  32. package/node_modules/@contrail/loader/lib/entities-v2/linesheet-v2/item-processor.js +8 -22
  33. package/node_modules/@contrail/loader/lib/entities-v2/linesheet-v2/item-processor.spec.js +43 -1
  34. package/node_modules/@contrail/loader/lib/pipeline/identifier-resolver.d.ts +5 -0
  35. package/node_modules/@contrail/loader/lib/pipeline/identifier-resolver.js +562 -0
  36. package/node_modules/@contrail/loader/lib/pipeline/identifier-resolver.spec.js +231 -0
  37. package/node_modules/@contrail/loader/lib/pipeline/item-execute-helpers.js +77 -3
  38. package/node_modules/@contrail/loader/lib/pipeline/item-execute-helpers.spec.js +82 -0
  39. package/node_modules/@contrail/loader/lib/pipeline/preview.js +0 -5
  40. package/node_modules/@contrail/loader/lib/pipeline/preview.spec.js +2 -2
  41. package/node_modules/@contrail/loader/lib/pipeline/project-item-execute-helpers.js +1 -1
  42. package/node_modules/@contrail/loader/lib/pipeline/transform.js +18 -0
  43. package/node_modules/@contrail/loader/lib/pipeline/transform.spec.js +3 -2
  44. package/node_modules/@contrail/loader/lib/pipeline/types.d.ts +5 -2
  45. package/node_modules/@contrail/loader/lib/pipeline/types.js +2 -0
  46. package/node_modules/@contrail/loader/lib/pipeline/validate.js +184 -437
  47. package/node_modules/@contrail/loader/lib/pipeline/validate.spec.js +157 -53
  48. package/node_modules/@contrail/loader/package.json +3 -2
  49. package/node_modules/@contrail/sdk/.github/workflows/publish-to-npm.yml +120 -0
  50. package/node_modules/@contrail/sdk/lib/auth/login.js +42 -3
  51. package/node_modules/@contrail/sdk/lib/core/request.d.ts +3 -0
  52. package/node_modules/@contrail/sdk/lib/core/request.js +100 -4
  53. package/node_modules/@contrail/sdk/lib/entities/entities.client.js +1 -1
  54. package/node_modules/@contrail/sdk/lib/entities/entity.resource.registry.js +1 -0
  55. package/node_modules/@contrail/sdk/lib/files/files.d.ts +13 -3
  56. package/node_modules/@contrail/sdk/lib/files/files.js +66 -25
  57. package/node_modules/@contrail/sdk/package.json +4 -1
  58. package/node_modules/validate-color/LICENSE +21 -0
  59. package/node_modules/validate-color/README.md +265 -0
  60. package/node_modules/validate-color/lib/index.d.ts +10 -0
  61. package/node_modules/validate-color/lib/index.js +1 -0
  62. package/node_modules/validate-color/package.json +91 -0
  63. package/node_modules/validate-color/test/redos.js +29 -0
  64. package/oclif.manifest.json +5 -80
  65. package/package.json +3 -6
  66. package/dist/api/dash0/client.d.ts +0 -13
  67. package/dist/api/dash0/client.js +0 -53
  68. package/dist/api/dash0/spans.d.ts +0 -8
  69. package/dist/api/dash0/spans.js +0 -91
  70. package/dist/api/dash0/types.d.ts +0 -84
  71. package/dist/api/dash0/types.js +0 -4
  72. package/dist/commands/event-workflows/assess-impact.d.ts +0 -19
  73. package/dist/commands/event-workflows/assess-impact.js +0 -104
  74. package/dist/helpers/event-workflow-impact/aggregate.d.ts +0 -23
  75. package/dist/helpers/event-workflow-impact/aggregate.js +0 -49
  76. package/dist/helpers/event-workflow-impact/aggregate.spec.js +0 -109
  77. package/dist/helpers/event-workflow-impact/classify.d.ts +0 -13
  78. package/dist/helpers/event-workflow-impact/classify.js +0 -135
  79. package/dist/helpers/event-workflow-impact/classify.spec.js +0 -138
  80. package/dist/helpers/event-workflow-impact/render.d.ts +0 -12
  81. package/dist/helpers/event-workflow-impact/render.js +0 -62
  82. package/dist/helpers/event-workflow-impact/subtree.d.ts +0 -20
  83. package/dist/helpers/event-workflow-impact/subtree.js +0 -93
  84. package/dist/helpers/event-workflow-impact/subtree.spec.d.ts +0 -1
  85. package/dist/helpers/event-workflow-impact/subtree.spec.js +0 -103
  86. /package/{dist/helpers/event-workflow-impact/aggregate.spec.d.ts → node_modules/@contrail/loader/lib/common/color-validation.spec.d.ts} +0 -0
  87. /package/{dist/helpers/event-workflow-impact/classify.spec.d.ts → node_modules/@contrail/loader/lib/pipeline/identifier-resolver.spec.d.ts} +0 -0
@@ -1,135 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizeRouteName = exports.looksLikeId = exports.templatePath = exports.classifySpan = exports.CATEGORY_LABEL = exports.CATEGORY_ORDER = void 0;
4
- exports.CATEGORY_ORDER = [
5
- 'http_direct',
6
- 'http_transitive',
7
- 'opensearch',
8
- 'dynamodb',
9
- 's3',
10
- 'timestream',
11
- 'lambda',
12
- 'other',
13
- ];
14
- exports.CATEGORY_LABEL = {
15
- http_direct: 'HTTP — direct (called by the action)',
16
- http_transitive: 'HTTP — transitive (called by downstream services)',
17
- opensearch: 'OpenSearch',
18
- dynamodb: 'DynamoDB',
19
- s3: 'S3',
20
- timestream: 'Timestream',
21
- lambda: 'Lambda invokes',
22
- other: 'Other',
23
- };
24
- function classifySpan(span) {
25
- const name = span.name;
26
- const service = span.resourceAttrs['service.name'] ?? '';
27
- const httpTarget = span.attrs['http.target'] ?? '';
28
- // Skip SERVER spans entirely — they are the receive-side of an HTTP call we
29
- // already counted on the client side, plus their internal DB ops are children
30
- // and will be classified on their own.
31
- if (span.kind === 2 /* SERVER */)
32
- return null;
33
- // OpenSearch HTTP request — match the actual /<index>/_search call,
34
- // not the wrapping contrail-open-search lambda span.
35
- if (httpTarget && /\/_search(\?|$)/.test(httpTarget)) {
36
- const method = span.attrs['http.request.method'] ?? 'GET';
37
- return {
38
- category: 'opensearch',
39
- service: 'opensearch',
40
- operation: `${method} ${templatePath(httpTarget)}`,
41
- };
42
- }
43
- // AWS SDK spans expose service via span name prefixes like `DynamoDB.Query`,
44
- // `S3.PutObject`, `TimestreamWrite.WriteRecords`.
45
- if (name.startsWith('DynamoDB.')) {
46
- return { category: 'dynamodb', service: 'dynamodb', operation: name };
47
- }
48
- if (name.startsWith('S3.')) {
49
- return { category: 's3', service: 's3', operation: name };
50
- }
51
- if (name.startsWith('Timestream')) {
52
- return { category: 'timestream', service: 'timestream', operation: name };
53
- }
54
- const isHttp = span.attrs['dash0.span.type'] === 'http' ||
55
- span.attrs['http.request.method'] !== undefined ||
56
- span.attrs['http.host'] !== undefined;
57
- if (isHttp) {
58
- const method = (span.attrs['http.request.method'] ?? 'GET').toUpperCase();
59
- const target = httpTarget || span.attrs['url.full'] || '';
60
- const path = templatePath(target);
61
- // Span name is sometimes just the method (e.g. "GET"), sometimes the full
62
- // route (e.g. "get /prod/api/items"). Build the operation from method+path
63
- // when we have a path; otherwise normalize the leading verb in name so
64
- // "get /x" and "GET /x" don't split into two buckets.
65
- const operation = path ? `${method} ${path}` : normalizeRouteName(name);
66
- // Direct HTTP = called by the action's own code (no SERVER boundary
67
- // between this span and the action root). Transitive = inside a service
68
- // the action invoked (1+ SERVER ancestors). Spans without subtree info
69
- // (e.g. unit tests) default to direct.
70
- const serverAncestors = span.serverAncestorCount ?? 0;
71
- return {
72
- category: serverAncestors === 0 ? 'http_direct' : 'http_transitive',
73
- service: service || 'unknown',
74
- operation,
75
- };
76
- }
77
- // Lambda-to-lambda invokes appear as CLIENT spans on aws.lambda resources
78
- // without HTTP attributes.
79
- const isLambdaCall = span.kind === 3 /* CLIENT */ &&
80
- span.resourceAttrs['dash0.resource.type'] === 'aws.lambda';
81
- if (isLambdaCall) {
82
- return { category: 'lambda', service: service || 'unknown', operation: name };
83
- }
84
- return { category: 'other', service: service || 'unknown', operation: name };
85
- }
86
- exports.classifySpan = classifySpan;
87
- const HTTP_METHOD_PREFIX = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/i;
88
- function templatePath(target) {
89
- if (!target)
90
- return '';
91
- // Strip query string.
92
- let path = target.split('?', 1)[0];
93
- // Drop scheme+host if present.
94
- if (path.startsWith('http')) {
95
- try {
96
- path = new URL(path).pathname;
97
- }
98
- catch {
99
- /* ignore */
100
- }
101
- }
102
- return path
103
- .split('/')
104
- .map((seg) => (looksLikeId(seg) ? ':id' : seg))
105
- .join('/');
106
- }
107
- exports.templatePath = templatePath;
108
- // A segment is treated as an opaque ID (and replaced with `:id`) only when it
109
- // looks like one. Long kebab-case route names like `assortment-items` should
110
- // stay readable, so we require numeric content or non-trivial mixed case.
111
- function looksLikeId(seg) {
112
- if (seg.length === 0)
113
- return false;
114
- if (/^\d+$/.test(seg))
115
- return true; // pure numeric
116
- if (/^[0-9a-f]{32}$/i.test(seg))
117
- return true; // 32-hex (trace IDs etc)
118
- // UUIDs.
119
- if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg)) {
120
- return true;
121
- }
122
- // Mixed case + length suggests an opaque ID (e.g. "cl3MF7mosBf8-Yf9").
123
- // Pure-lowercase or pure-uppercase with hyphens stays a route name.
124
- if (seg.length >= 12 && /[A-Z]/.test(seg) && /[a-z]/.test(seg))
125
- return true;
126
- // Long alphanumeric without separators is suspicious.
127
- if (seg.length >= 16 && /^[A-Za-z0-9]+$/.test(seg) && /\d/.test(seg))
128
- return true;
129
- return false;
130
- }
131
- exports.looksLikeId = looksLikeId;
132
- function normalizeRouteName(name) {
133
- return name.replace(HTTP_METHOD_PREFIX, (m) => m.toUpperCase());
134
- }
135
- exports.normalizeRouteName = normalizeRouteName;
@@ -1,138 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const classify_1 = require("./classify");
4
- function span(partial = {}) {
5
- return {
6
- name: 'unnamed',
7
- traceId: 't',
8
- spanId: 's',
9
- parentSpanId: '',
10
- kind: 3,
11
- startNs: 0n,
12
- endNs: 0n,
13
- durationNs: 0n,
14
- attrs: {},
15
- resourceAttrs: {},
16
- ...partial,
17
- };
18
- }
19
- describe('templatePath', () => {
20
- it('strips query string', () => {
21
- expect((0, classify_1.templatePath)('/api/items?x=1')).toBe('/api/items');
22
- });
23
- it('replaces id-shaped segments with :id', () => {
24
- expect((0, classify_1.templatePath)('/api/items/cl3MF7mosBf8-Yf9')).toBe('/api/items/:id');
25
- expect((0, classify_1.templatePath)('/api/items/123456')).toBe('/api/items/:id');
26
- });
27
- it('keeps short alphabetic segments untouched', () => {
28
- expect((0, classify_1.templatePath)('/api/items/foo')).toBe('/api/items/foo');
29
- });
30
- it('keeps long kebab-case route names untouched', () => {
31
- expect((0, classify_1.templatePath)('/prod/api/assortment-items/batch')).toBe('/prod/api/assortment-items/batch');
32
- expect((0, classify_1.templatePath)('/prod/api/project-items')).toBe('/prod/api/project-items');
33
- });
34
- it('replaces UUID segments', () => {
35
- expect((0, classify_1.templatePath)('/api/things/550e8400-e29b-41d4-a716-446655440000')).toBe('/api/things/:id');
36
- });
37
- it('handles full URLs', () => {
38
- expect((0, classify_1.templatePath)('https://api.vibeiq.com/prod/api/items?x=1')).toBe('/prod/api/items');
39
- });
40
- });
41
- describe('classifySpan', () => {
42
- it('classifies OpenSearch /_search HTTP spans', () => {
43
- const s = span({
44
- name: 'GET',
45
- attrs: {
46
- 'http.target': '/gap1/_search',
47
- 'http.request.method': 'GET',
48
- },
49
- });
50
- expect((0, classify_1.classifySpan)(s)).toEqual({
51
- category: 'opensearch',
52
- service: 'opensearch',
53
- operation: 'GET /gap1/_search',
54
- });
55
- });
56
- it('classifies DynamoDB AWS SDK spans by name prefix', () => {
57
- expect((0, classify_1.classifySpan)(span({ name: 'DynamoDB.Query' }))).toEqual({
58
- category: 'dynamodb',
59
- service: 'dynamodb',
60
- operation: 'DynamoDB.Query',
61
- });
62
- expect((0, classify_1.classifySpan)(span({ name: 'DynamoDB.UpdateItem' }))?.category).toBe('dynamodb');
63
- });
64
- it('classifies S3 AWS SDK spans', () => {
65
- expect((0, classify_1.classifySpan)(span({ name: 'S3.PutObject' }))?.category).toBe('s3');
66
- });
67
- it('classifies Timestream AWS SDK spans', () => {
68
- expect((0, classify_1.classifySpan)(span({ name: 'TimestreamWrite.WriteRecords' }))?.category).toBe('timestream');
69
- });
70
- it('classifies HTTP with no SERVER ancestor as http_direct', () => {
71
- // Apps lambda's outbound HTTP CLIENT — sits below the action span via a
72
- // lambda-invoke CLIENT, no SERVER boundary in between.
73
- const s = span({
74
- name: 'GET',
75
- attrs: {
76
- 'http.request.method': 'GET',
77
- 'http.target': '/prod/api/items?relations=&criteria=...',
78
- 'http.host': 'api.vibeiq.com:443',
79
- },
80
- resourceAttrs: { 'service.name': 'contrail-event-workflow-apps' },
81
- serverAncestorCount: 0,
82
- });
83
- expect((0, classify_1.classifySpan)(s)).toEqual({
84
- category: 'http_direct',
85
- service: 'contrail-event-workflow-apps',
86
- operation: 'GET /prod/api/items',
87
- });
88
- });
89
- it('classifies HTTP behind a SERVER boundary as http_transitive', () => {
90
- // Outbound HTTP from a downstream service the action called — there is a
91
- // SERVER span between this and the action.
92
- const s = span({
93
- name: 'GET',
94
- attrs: {
95
- 'http.request.method': 'GET',
96
- 'http.target': '/prod/api/open-search',
97
- 'http.host': 'api.vibeiq.com:443',
98
- },
99
- resourceAttrs: { 'service.name': 'contrail-items' },
100
- serverAncestorCount: 1,
101
- });
102
- expect((0, classify_1.classifySpan)(s)?.category).toBe('http_transitive');
103
- });
104
- it('classifies lambda-to-lambda invokes (no HTTP attrs, aws.lambda resource)', () => {
105
- const s = span({
106
- name: 'contrail-items-prod-api',
107
- kind: 3,
108
- resourceAttrs: {
109
- 'service.name': 'contrail-items',
110
- 'dash0.resource.type': 'aws.lambda',
111
- },
112
- });
113
- expect((0, classify_1.classifySpan)(s)).toEqual({
114
- category: 'lambda',
115
- service: 'contrail-items',
116
- operation: 'contrail-items-prod-api',
117
- });
118
- });
119
- it('drops SERVER-side spans (already represented by client side)', () => {
120
- expect((0, classify_1.classifySpan)(span({ kind: 2, name: 'GET /prod/api/items' }))).toBeNull();
121
- });
122
- it('uppercases the HTTP method when falling back to span.name', () => {
123
- // Some spans have only http.host set (not http.target/method) and a
124
- // lowercase verb in the span name. They should bucket with their uppercase
125
- // siblings rather than splitting.
126
- const lower = span({
127
- name: 'get /prod/api/custom-entities',
128
- attrs: { 'http.host': 'api.vibeiq.com:443' },
129
- resourceAttrs: { 'service.name': 'contrail-custom-entities' },
130
- serverAncestorCount: 1,
131
- });
132
- expect((0, classify_1.classifySpan)(lower)?.operation).toBe('GET /prod/api/custom-entities');
133
- expect((0, classify_1.classifySpan)(lower)?.category).toBe('http_transitive');
134
- });
135
- it('falls back to "other" for unknown CLIENT spans without aws.lambda resource', () => {
136
- expect((0, classify_1.classifySpan)(span({ name: 'mystery-step' }))?.category).toBe('other');
137
- });
138
- });
@@ -1,12 +0,0 @@
1
- import { AggregateResult } from './aggregate';
2
- export interface RenderOptions {
3
- identifier: string;
4
- dataset: string;
5
- lookback: string;
6
- samplesRequested: number;
7
- samplesObtained: number;
8
- tracesDropped: number;
9
- totalOrphanCount: number;
10
- preciseRootCount: number;
11
- }
12
- export declare function renderReport(result: AggregateResult, opts: RenderOptions): string;
@@ -1,62 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.renderReport = void 0;
4
- const tslib_1 = require("tslib");
5
- const cli_table3_1 = tslib_1.__importDefault(require("cli-table3"));
6
- const classify_1 = require("./classify");
7
- function renderReport(result, opts) {
8
- const lines = [];
9
- lines.push(`Action: ${opts.identifier}`);
10
- lines.push(`Dataset: ${opts.dataset}`);
11
- lines.push(`Lookback: ${opts.lookback}`);
12
- lines.push(`Samples: ${opts.samplesObtained}/${opts.samplesRequested} (${opts.tracesDropped} dropped)`);
13
- if (opts.preciseRootCount < opts.samplesObtained) {
14
- const fallback = opts.samplesObtained - opts.preciseRootCount;
15
- lines.push(`Rooting: ${opts.preciseRootCount} precise / ${fallback} fallback (no inner action span — framework AWS-SDK direct children stripped heuristically)`);
16
- }
17
- lines.push('');
18
- lines.push('Per-invocation downstream calls. "% of traces" is how often the bucket appears in any invocation; median/p95/max include zeros.');
19
- lines.push('');
20
- const byCategory = new Map();
21
- for (const b of result.buckets) {
22
- const arr = byCategory.get(b.key.category) ?? [];
23
- arr.push(b);
24
- byCategory.set(b.key.category, arr);
25
- }
26
- let any = false;
27
- for (const cat of classify_1.CATEGORY_ORDER) {
28
- const buckets = byCategory.get(cat);
29
- if (!buckets || buckets.length === 0)
30
- continue;
31
- any = true;
32
- lines.push(classify_1.CATEGORY_LABEL[cat]);
33
- const table = new cli_table3_1.default({
34
- head: ['service', 'operation', 'median', 'p95', 'max', '% of traces'],
35
- style: { head: [], border: [] },
36
- });
37
- buckets
38
- .sort((a, b) => b.p95 - a.p95 || b.max - a.max)
39
- .forEach((b) => table.push([
40
- b.key.service,
41
- b.key.operation,
42
- fmt(b.median),
43
- fmt(b.p95),
44
- fmt(b.max),
45
- `${(b.coverage * 100).toFixed(0)}%`,
46
- ]));
47
- lines.push(table.toString());
48
- lines.push('');
49
- }
50
- if (!any) {
51
- lines.push(' (no descendant spans observed in the action subtree)');
52
- lines.push('');
53
- }
54
- if (opts.totalOrphanCount > 0) {
55
- lines.push(`Note: ${opts.totalOrphanCount} span(s) across sampled traces have a parent ID we never observed — likely propagation gaps. Numbers above may undercount.`);
56
- }
57
- return lines.join('\n');
58
- }
59
- exports.renderReport = renderReport;
60
- function fmt(n) {
61
- return Number.isInteger(n) ? n.toString() : n.toFixed(1);
62
- }
@@ -1,20 +0,0 @@
1
- import { FlatSpan } from '../../api/dash0/types';
2
- export interface SubtreeResult {
3
- descendants: FlatSpan[];
4
- actionSpan: FlatSpan;
5
- preciseRoot: boolean;
6
- orphanCount: number;
7
- }
8
- /**
9
- * Walk descendants under the span carrying the action identifier.
10
- *
11
- * The action identifier is set on the workflow consumer span (the lambda root)
12
- * AND echoed onto a child PRODUCER span named after the action identifier itself.
13
- * We root from whichever bears `contrail.event_workflow.action.identifier` AND has
14
- * a span name matching the identifier (the inner action span). If we can't find
15
- * that, we fall back to the consumer-level span.
16
- *
17
- * The returned descendants set excludes the framework consumer overhead — only
18
- * what the action's code touched.
19
- */
20
- export declare function extractActionSubtree(spans: FlatSpan[], identifier: string): SubtreeResult | null;
@@ -1,93 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractActionSubtree = void 0;
4
- // AWS SDK span name prefixes that, when emitted as a *direct* child of the
5
- // consumer span, represent the workflow framework's own infrastructure
6
- // (task-state DDB writes, S3 payload staging, Timestream metrics, queue ops),
7
- // not customer code. These are stripped (along with their subtrees) when we
8
- // have to fall back to the consumer span as the root.
9
- const FRAMEWORK_AWS_SDK_PREFIXES = ['DynamoDB.', 'S3.', 'Timestream', 'SQS.', 'SNS.'];
10
- function isFrameworkAwsSdk(span) {
11
- return FRAMEWORK_AWS_SDK_PREFIXES.some((p) => span.name.startsWith(p));
12
- }
13
- /**
14
- * Walk descendants under the span carrying the action identifier.
15
- *
16
- * The action identifier is set on the workflow consumer span (the lambda root)
17
- * AND echoed onto a child PRODUCER span named after the action identifier itself.
18
- * We root from whichever bears `contrail.event_workflow.action.identifier` AND has
19
- * a span name matching the identifier (the inner action span). If we can't find
20
- * that, we fall back to the consumer-level span.
21
- *
22
- * The returned descendants set excludes the framework consumer overhead — only
23
- * what the action's code touched.
24
- */
25
- function extractActionSubtree(spans, identifier) {
26
- if (spans.length === 0)
27
- return null;
28
- const byParent = new Map();
29
- const byId = new Map();
30
- for (const s of spans) {
31
- byId.set(s.spanId, s);
32
- if (s.parentSpanId) {
33
- const arr = byParent.get(s.parentSpanId) ?? [];
34
- arr.push(s);
35
- byParent.set(s.parentSpanId, arr);
36
- }
37
- }
38
- // The inner action span is named exactly after the identifier (PRODUCER kind,
39
- // child of the consumer lambda). When that exists, walking it yields exactly
40
- // the action's downstream calls.
41
- const inner = spans.find((s) => s.name === identifier);
42
- // The consumer/root span carries `contrail.event_workflow.action.identifier`
43
- // as an attribute. Some action variants run inline in the consumer (no
44
- // inner PRODUCER span); for those we root at the consumer and strip the
45
- // framework's direct AWS-SDK children.
46
- const carrier = spans.find((s) => s.attrs['contrail.event_workflow.action.identifier'] === identifier);
47
- const actionSpan = inner ?? carrier;
48
- if (!actionSpan)
49
- return null;
50
- const preciseRoot = !!inner;
51
- const descendants = [];
52
- // BFS tracking depth and "service boundary crossings" level-by-level. A
53
- // service boundary is a SERVER span (kind=2) that represents code running
54
- // in another service. The action's own executor SERVER — the SERVER span
55
- // emitted on the receive side of the lambda invocation that runs the
56
- // action — is NOT counted, because it represents the action's own
57
- // execution context, not downstream work.
58
- const queue = [
59
- { spanId: actionSpan.spanId, depth: 0, serverAncestors: 0 },
60
- ];
61
- while (queue.length) {
62
- const { spanId: parentId, depth, serverAncestors } = queue.shift();
63
- const children = byParent.get(parentId) ?? [];
64
- for (const c of children) {
65
- // Fallback path: when rooting at the consumer, AWS-SDK spans that are a
66
- // *direct* child of the consumer are framework infrastructure (task-state
67
- // DDB writes, S3 payload staging, Timestream metrics). Skip them and
68
- // their subtrees. Nested AWS-SDK spans (e.g. DDB queries inside a
69
- // downstream contrail-items lambda) are kept.
70
- if (!preciseRoot && parentId === actionSpan.spanId && isFrameworkAwsSdk(c)) {
71
- continue;
72
- }
73
- c.depthFromAction = depth + 1;
74
- c.serverAncestorCount = serverAncestors;
75
- descendants.push(c);
76
- // Skip the action's executor SERVER from boundary counting: a SERVER
77
- // span that is a *direct* child of a non-SERVER action span is the
78
- // server side of the invocation that runs the action's code itself.
79
- const isExecutorServer = c.kind === 2 &&
80
- parentId === actionSpan.spanId &&
81
- actionSpan.kind !== 2;
82
- const childServerCount = c.kind === 2 && !isExecutorServer ? serverAncestors + 1 : serverAncestors;
83
- queue.push({ spanId: c.spanId, depth: depth + 1, serverAncestors: childServerCount });
84
- }
85
- }
86
- let orphanCount = 0;
87
- for (const s of spans) {
88
- if (s.parentSpanId && !byId.has(s.parentSpanId))
89
- orphanCount++;
90
- }
91
- return { descendants, actionSpan, preciseRoot, orphanCount };
92
- }
93
- exports.extractActionSubtree = extractActionSubtree;
@@ -1 +0,0 @@
1
- export {};
@@ -1,103 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const subtree_1 = require("./subtree");
4
- const IDENT = '@gap-automations:exampleAction';
5
- function s(part) {
6
- return {
7
- name: part.spanId,
8
- traceId: 't',
9
- parentSpanId: '',
10
- kind: 3,
11
- startNs: 0n,
12
- endNs: 0n,
13
- durationNs: 0n,
14
- attrs: {},
15
- resourceAttrs: {},
16
- ...part,
17
- };
18
- }
19
- describe('extractActionSubtree', () => {
20
- it('returns null when no candidate exists', () => {
21
- expect((0, subtree_1.extractActionSubtree)([s({ spanId: 'a' })], IDENT)).toBeNull();
22
- });
23
- describe('precise root (inner action span exists)', () => {
24
- it('does not count the action executor SERVER as a service boundary', () => {
25
- // Realistic Lambda shape: action span (kind=CLIENT, the invocation) has
26
- // a SERVER child representing the receive-side of that invocation —
27
- // this is the action's own executor lambda. The HTTP outbound from
28
- // inside that lambda must be classified as direct (serverAncestorCount=0).
29
- // Only SERVER spans BELOW outbound calls (downstream services) count.
30
- const spans = [
31
- s({
32
- spanId: 'consumer',
33
- parentSpanId: '',
34
- name: 'consumer',
35
- kind: 2,
36
- attrs: { 'contrail.event_workflow.action.identifier': IDENT },
37
- }),
38
- s({ spanId: 'inner', parentSpanId: 'consumer', name: IDENT, kind: 3 }),
39
- // Action's own executor SERVER (apps lambda receive side) — exempt.
40
- s({ spanId: 'execSrv', parentSpanId: 'inner', name: 'apps-lambda', kind: 2 }),
41
- // Action code's own outbound HTTP — direct.
42
- s({ spanId: 'http1', parentSpanId: 'execSrv', name: 'GET', kind: 3 }),
43
- // Downstream SERVER (e.g. contrail-items receiving the request).
44
- s({ spanId: 'downSrv', parentSpanId: 'http1', name: 'GET /api/items', kind: 2 }),
45
- // HTTP outbound from downstream — transitive.
46
- s({ spanId: 'http2', parentSpanId: 'downSrv', name: 'GET', kind: 3 }),
47
- ];
48
- const result = (0, subtree_1.extractActionSubtree)(spans, IDENT);
49
- expect(result).not.toBeNull();
50
- expect(result.preciseRoot).toBe(true);
51
- const find = (id) => result.descendants.find((d) => d.spanId === id);
52
- // Executor SERVER is exempt: it gets count 0 itself, and its descendants
53
- // don't see +1 from it.
54
- expect(find('execSrv').serverAncestorCount).toBe(0);
55
- expect(find('http1').serverAncestorCount).toBe(0);
56
- // The downstream SERVER does count for its descendants.
57
- expect(find('downSrv').serverAncestorCount).toBe(0);
58
- expect(find('http2').serverAncestorCount).toBe(1);
59
- });
60
- });
61
- describe('fallback root (no inner span)', () => {
62
- it('walks consumer subtree but strips direct AWS-SDK children', () => {
63
- // consumer carries the identifier attr.
64
- // direct children: framework DDB, framework S3, framework Timestream,
65
- // http GET (action work)
66
- // http GET -> nested DDB (downstream's own DDB; should be kept)
67
- const spans = [
68
- s({
69
- spanId: 'consumer',
70
- parentSpanId: '',
71
- name: 'consumer-lambda',
72
- attrs: { 'contrail.event_workflow.action.identifier': IDENT },
73
- }),
74
- s({ spanId: 'fwDdb', parentSpanId: 'consumer', name: 'DynamoDB.Query' }),
75
- s({ spanId: 'fwS3', parentSpanId: 'consumer', name: 'S3.GetObject' }),
76
- s({
77
- spanId: 'fwTs',
78
- parentSpanId: 'consumer',
79
- name: 'TimestreamWrite.WriteRecords',
80
- }),
81
- s({ spanId: 'http1', parentSpanId: 'consumer', name: 'GET' }),
82
- s({ spanId: 'nestedDdb', parentSpanId: 'http1', name: 'DynamoDB.Query' }),
83
- ];
84
- const result = (0, subtree_1.extractActionSubtree)(spans, IDENT);
85
- expect(result).not.toBeNull();
86
- expect(result.preciseRoot).toBe(false);
87
- const ids = result.descendants.map((d) => d.spanId).sort();
88
- expect(ids).toEqual(['http1', 'nestedDdb']);
89
- });
90
- });
91
- it('counts orphans (parent_id pointing outside the trace)', () => {
92
- const spans = [
93
- s({
94
- spanId: 'consumer',
95
- parentSpanId: '',
96
- attrs: { 'contrail.event_workflow.action.identifier': IDENT },
97
- }),
98
- s({ spanId: 'unattached', parentSpanId: 'missing-parent' }),
99
- ];
100
- const result = (0, subtree_1.extractActionSubtree)(spans, IDENT);
101
- expect(result.orphanCount).toBe(1);
102
- });
103
- });