@testsmith/perfornium 0.6.3 → 0.6.5
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/dist/cli/commands/distributed.js +2 -2
- package/dist/cli/commands/report.js +2 -2
- package/dist/cli/commands/run.js +2 -0
- package/dist/config/parser.js +2 -2
- package/dist/config/types/global-config.d.ts +82 -2
- package/dist/config/types/scenario-config.d.ts +2 -2
- package/dist/core/data/data-manager.d.ts +70 -0
- package/dist/core/data/data-manager.js +186 -0
- package/dist/core/data/data-provider.d.ts +85 -0
- package/dist/core/data/data-provider.js +468 -0
- package/dist/core/data/index.d.ts +8 -0
- package/dist/core/data/index.js +13 -0
- package/dist/core/execution/check-evaluator.d.ts +10 -0
- package/dist/core/execution/check-evaluator.js +79 -0
- package/dist/core/execution/data-extractor.d.ts +6 -0
- package/dist/core/execution/data-extractor.js +70 -0
- package/dist/core/execution/index.d.ts +3 -0
- package/dist/core/execution/index.js +9 -0
- package/dist/core/execution/json-payload-processor.d.ts +7 -0
- package/dist/core/execution/json-payload-processor.js +140 -0
- package/dist/core/factories/index.d.ts +2 -0
- package/dist/core/factories/index.js +7 -0
- package/dist/core/factories/output-handler-factory.d.ts +10 -0
- package/dist/core/factories/output-handler-factory.js +91 -0
- package/dist/core/factories/protocol-handler-factory.d.ts +12 -0
- package/dist/core/factories/protocol-handler-factory.js +96 -0
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.js +8 -3
- package/dist/core/reporting/dashboard-reporter.d.ts +17 -0
- package/dist/core/reporting/dashboard-reporter.js +127 -0
- package/dist/core/reporting/index.d.ts +1 -0
- package/dist/core/reporting/index.js +5 -0
- package/dist/core/step-executor.d.ts +6 -20
- package/dist/core/step-executor.js +72 -366
- package/dist/core/strategies/index.d.ts +2 -0
- package/dist/core/strategies/index.js +7 -0
- package/dist/core/strategies/scenario-selector.d.ts +13 -0
- package/dist/core/strategies/scenario-selector.js +37 -0
- package/dist/core/strategies/think-time-strategy.d.ts +15 -0
- package/dist/core/strategies/think-time-strategy.js +71 -0
- package/dist/core/test-runner.d.ts +4 -11
- package/dist/core/test-runner.js +105 -312
- package/dist/core/virtual-user.d.ts +7 -37
- package/dist/core/virtual-user.js +29 -269
- package/dist/dashboard/routes/api.d.ts +64 -0
- package/dist/dashboard/routes/api.js +569 -0
- package/dist/dashboard/routes/index.d.ts +2 -0
- package/dist/dashboard/routes/index.js +7 -0
- package/dist/dashboard/routes/static.d.ts +6 -0
- package/dist/dashboard/routes/static.js +76 -0
- package/dist/dashboard/server.d.ts +8 -84
- package/dist/dashboard/server.js +76 -2007
- package/dist/dashboard/services/file-scanner.d.ts +7 -0
- package/dist/dashboard/services/file-scanner.js +114 -0
- package/dist/dashboard/services/index.d.ts +5 -0
- package/dist/dashboard/services/index.js +13 -0
- package/dist/dashboard/services/influxdb-service.d.ts +41 -0
- package/dist/dashboard/services/influxdb-service.js +329 -0
- package/dist/dashboard/services/metrics-parser.d.ts +12 -0
- package/dist/dashboard/services/metrics-parser.js +209 -0
- package/dist/dashboard/services/results-manager.d.ts +17 -0
- package/dist/dashboard/services/results-manager.js +311 -0
- package/dist/dashboard/services/test-executor.d.ts +41 -0
- package/dist/dashboard/services/test-executor.js +250 -0
- package/dist/dashboard/services/workers-manager.d.ts +13 -0
- package/dist/dashboard/services/workers-manager.js +81 -0
- package/dist/dashboard/templates/index.html +122 -0
- package/dist/dashboard/templates/scripts/main.js +3280 -0
- package/dist/dashboard/templates/styles.css +402 -0
- package/dist/dashboard/types.d.ts +168 -0
- package/dist/dashboard/types.js +2 -0
- package/dist/distributed/result-aggregator.js +1 -3
- package/dist/metrics/batch/batch-processor.d.ts +27 -0
- package/dist/metrics/batch/batch-processor.js +85 -0
- package/dist/metrics/batch/index.d.ts +1 -0
- package/dist/metrics/batch/index.js +5 -0
- package/dist/metrics/collector.d.ts +46 -45
- package/dist/metrics/collector.js +179 -640
- package/dist/metrics/core/error-tracker.d.ts +9 -0
- package/dist/metrics/core/error-tracker.js +52 -0
- package/dist/metrics/core/index.d.ts +3 -0
- package/dist/metrics/core/index.js +9 -0
- package/dist/metrics/core/result-storage.d.ts +19 -0
- package/dist/metrics/core/result-storage.js +56 -0
- package/dist/metrics/core/statistics-engine.d.ts +27 -0
- package/dist/metrics/core/statistics-engine.js +91 -0
- package/dist/metrics/output/file-writer.d.ts +19 -0
- package/dist/metrics/output/file-writer.js +129 -0
- package/dist/metrics/output/index.d.ts +2 -0
- package/dist/metrics/output/index.js +10 -0
- package/dist/metrics/output/influxdb-writer.d.ts +89 -0
- package/dist/metrics/output/influxdb-writer.js +404 -0
- package/dist/metrics/realtime/dispatcher.d.ts +18 -0
- package/dist/metrics/realtime/dispatcher.js +45 -0
- package/dist/metrics/realtime/endpoints/graphite.d.ts +3 -0
- package/dist/metrics/realtime/endpoints/graphite.js +61 -0
- package/dist/metrics/realtime/endpoints/influxdb.d.ts +3 -0
- package/dist/metrics/realtime/endpoints/influxdb.js +35 -0
- package/dist/metrics/realtime/endpoints/webhook.d.ts +3 -0
- package/dist/metrics/realtime/endpoints/webhook.js +22 -0
- package/dist/metrics/realtime/endpoints/websocket.d.ts +3 -0
- package/dist/metrics/realtime/endpoints/websocket.js +25 -0
- package/dist/metrics/realtime/index.d.ts +5 -0
- package/dist/metrics/realtime/index.js +13 -0
- package/dist/metrics/reporting/index.d.ts +3 -0
- package/dist/metrics/reporting/index.js +9 -0
- package/dist/metrics/reporting/step-statistics.d.ts +6 -0
- package/dist/metrics/reporting/step-statistics.js +59 -0
- package/dist/metrics/reporting/summary-generator.d.ts +16 -0
- package/dist/metrics/reporting/summary-generator.js +46 -0
- package/dist/metrics/reporting/timeline-calculator.d.ts +7 -0
- package/dist/metrics/reporting/timeline-calculator.js +86 -0
- package/dist/metrics/types.d.ts +58 -0
- package/dist/outputs/csv.d.ts +2 -0
- package/dist/outputs/csv.js +21 -2
- package/dist/outputs/json.js +6 -2
- package/dist/protocols/rest/handler.d.ts +4 -53
- package/dist/protocols/rest/handler.js +73 -454
- package/dist/protocols/rest/request/auth-handler.d.ts +4 -0
- package/dist/protocols/rest/request/auth-handler.js +30 -0
- package/dist/protocols/rest/request/body-processor.d.ts +11 -0
- package/dist/protocols/rest/request/body-processor.js +62 -0
- package/dist/protocols/rest/request/index.d.ts +2 -0
- package/dist/protocols/rest/request/index.js +7 -0
- package/dist/protocols/rest/response/checks.d.ts +6 -0
- package/dist/protocols/rest/response/checks.js +71 -0
- package/dist/protocols/rest/response/index.d.ts +2 -0
- package/dist/protocols/rest/response/index.js +7 -0
- package/dist/protocols/rest/response/size-calculator.d.ts +12 -0
- package/dist/protocols/rest/response/size-calculator.js +64 -0
- package/dist/protocols/web/browser/highlight.d.ts +7 -0
- package/dist/protocols/web/browser/highlight.js +47 -0
- package/dist/protocols/web/browser/index.d.ts +4 -0
- package/dist/protocols/web/browser/index.js +11 -0
- package/dist/protocols/web/browser/manager.d.ts +20 -0
- package/dist/protocols/web/browser/manager.js +189 -0
- package/dist/protocols/web/browser/screenshot.d.ts +8 -0
- package/dist/protocols/web/browser/screenshot.js +69 -0
- package/dist/protocols/web/browser/storage.d.ts +5 -0
- package/dist/protocols/web/browser/storage.js +45 -0
- package/dist/protocols/web/commands/index.d.ts +5 -0
- package/dist/protocols/web/commands/index.js +11 -0
- package/dist/protocols/web/commands/interaction.d.ts +13 -0
- package/dist/protocols/web/commands/interaction.js +68 -0
- package/dist/protocols/web/commands/measurement.d.ts +16 -0
- package/dist/protocols/web/commands/measurement.js +33 -0
- package/dist/protocols/web/commands/navigation.d.ts +11 -0
- package/dist/protocols/web/commands/navigation.js +43 -0
- package/dist/protocols/web/commands/types.d.ts +12 -0
- package/dist/protocols/web/commands/types.js +2 -0
- package/dist/protocols/web/commands/verification.d.ts +11 -0
- package/dist/protocols/web/commands/verification.js +98 -0
- package/dist/protocols/web/handler.d.ts +19 -30
- package/dist/protocols/web/handler.js +160 -650
- package/dist/protocols/web/network/capture.d.ts +19 -0
- package/dist/protocols/web/network/capture.js +225 -0
- package/dist/protocols/web/network/filters.d.ts +5 -0
- package/dist/protocols/web/network/filters.js +49 -0
- package/dist/protocols/web/network/index.d.ts +4 -0
- package/dist/protocols/web/network/index.js +9 -0
- package/dist/protocols/web/network/types.d.ts +13 -0
- package/dist/protocols/web/network/types.js +2 -0
- package/dist/protocols/web/network/utils.d.ts +8 -0
- package/dist/protocols/web/network/utils.js +29 -0
- package/dist/recorder/native-recorder.js +2 -1
- package/dist/reporting/chart-data/index.d.ts +5 -0
- package/dist/reporting/chart-data/index.js +13 -0
- package/dist/reporting/chart-data/network.d.ts +25 -0
- package/dist/reporting/chart-data/network.js +78 -0
- package/dist/reporting/chart-data/scenario.d.ts +37 -0
- package/dist/reporting/chart-data/scenario.js +76 -0
- package/dist/reporting/chart-data/step-statistics.d.ts +24 -0
- package/dist/reporting/chart-data/step-statistics.js +94 -0
- package/dist/reporting/chart-data/throughput.d.ts +16 -0
- package/dist/reporting/chart-data/throughput.js +24 -0
- package/dist/reporting/chart-data/timeline.d.ts +17 -0
- package/dist/reporting/chart-data/timeline.js +46 -0
- package/dist/reporting/handlebars-helpers.d.ts +1 -0
- package/dist/reporting/handlebars-helpers.js +63 -0
- package/dist/reporting/{enhanced-html-generator.d.ts → html-generator.d.ts} +1 -1
- package/dist/reporting/{enhanced-html-generator.js → html-generator.js} +10 -7
- package/dist/reporting/templates/{enhanced-report.hbs → report.hbs} +9 -9
- package/dist/utils/data-utils.d.ts +17 -0
- package/dist/utils/data-utils.js +129 -0
- package/dist/utils/template.js +2 -2
- package/package.json +5 -2
- package/dist/core/csv-data-provider.d.ts +0 -47
- package/dist/core/csv-data-provider.js +0 -265
- package/dist/reporting/generator.d.ts +0 -42
- package/dist/reporting/generator.js +0 -1217
- package/dist/reporting/templates/html.hbs +0 -2453
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InfluxDBWriter = void 0;
|
|
4
|
+
exports.getInfluxDBWriter = getInfluxDBWriter;
|
|
5
|
+
exports.setInfluxDBWriter = setInfluxDBWriter;
|
|
6
|
+
exports.initInfluxDBWriter = initInfluxDBWriter;
|
|
7
|
+
const influxdb_client_1 = require("@influxdata/influxdb-client");
|
|
8
|
+
const logger_1 = require("../../utils/logger");
|
|
9
|
+
/**
|
|
10
|
+
* InfluxDB writer for test metrics (response times, network calls, etc.)
|
|
11
|
+
* This is optional - test results can still be stored as JSON files.
|
|
12
|
+
*/
|
|
13
|
+
class InfluxDBWriter {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.client = null;
|
|
16
|
+
this.writeApi = null;
|
|
17
|
+
this.queryApi = null;
|
|
18
|
+
this.enabled = false;
|
|
19
|
+
this.currentTestId = '';
|
|
20
|
+
this.currentTestName = '';
|
|
21
|
+
this.config = {
|
|
22
|
+
url: config?.url || process.env.INFLUXDB_URL || 'http://localhost:8086',
|
|
23
|
+
token: config?.token || process.env.INFLUXDB_TOKEN || '',
|
|
24
|
+
org: config?.org || process.env.INFLUXDB_ORG || 'perfornium',
|
|
25
|
+
bucket: config?.bucket || process.env.INFLUXDB_BUCKET || 'metrics',
|
|
26
|
+
batchSize: config?.batchSize || 100,
|
|
27
|
+
flushInterval: config?.flushInterval || 1000
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async connect() {
|
|
31
|
+
if (!this.config.token) {
|
|
32
|
+
logger_1.logger.debug('InfluxDB token not configured, test metrics will only be stored in files');
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
this.client = new influxdb_client_1.InfluxDB({
|
|
37
|
+
url: this.config.url,
|
|
38
|
+
token: this.config.token
|
|
39
|
+
});
|
|
40
|
+
this.writeApi = this.client.getWriteApi(this.config.org, this.config.bucket, 'ms', {
|
|
41
|
+
batchSize: this.config.batchSize,
|
|
42
|
+
flushInterval: this.config.flushInterval
|
|
43
|
+
});
|
|
44
|
+
this.queryApi = this.client.getQueryApi(this.config.org);
|
|
45
|
+
// Test connection
|
|
46
|
+
const query = `from(bucket: "${this.config.bucket}") |> range(start: -1s) |> limit(n: 1)`;
|
|
47
|
+
await this.queryApi.collectRows(query);
|
|
48
|
+
this.enabled = true;
|
|
49
|
+
logger_1.logger.info(`InfluxDB connected for test metrics at ${this.config.url}`);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
logger_1.logger.warn(`Failed to connect to InfluxDB for test metrics: ${error.message}`);
|
|
54
|
+
this.enabled = false;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
isEnabled() {
|
|
59
|
+
return this.enabled;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Start a new test run - sets the test ID and name for all subsequent writes
|
|
63
|
+
*/
|
|
64
|
+
startTest(testId, testName) {
|
|
65
|
+
this.currentTestId = testId;
|
|
66
|
+
this.currentTestName = testName;
|
|
67
|
+
logger_1.logger.debug(`InfluxDB writer started for test: ${testName} (${testId})`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Write a single test result to InfluxDB
|
|
71
|
+
*/
|
|
72
|
+
async writeResult(result) {
|
|
73
|
+
if (!this.enabled || !this.writeApi)
|
|
74
|
+
return;
|
|
75
|
+
try {
|
|
76
|
+
const point = new influxdb_client_1.Point('test_result')
|
|
77
|
+
.tag('test_id', this.currentTestId)
|
|
78
|
+
.tag('test_name', this.currentTestName)
|
|
79
|
+
.tag('scenario', result.scenario || 'default')
|
|
80
|
+
.tag('action', result.action || result.step_name || 'unknown')
|
|
81
|
+
.tag('success', result.success ? 'true' : 'false')
|
|
82
|
+
.intField('vu_id', result.vu_id)
|
|
83
|
+
.intField('iteration', result.iteration || 0)
|
|
84
|
+
.floatField('duration', result.duration || 0)
|
|
85
|
+
.intField('status', result.status || 0)
|
|
86
|
+
.timestamp(new Date(result.timestamp));
|
|
87
|
+
if (result.response_size) {
|
|
88
|
+
point.intField('response_size', result.response_size);
|
|
89
|
+
}
|
|
90
|
+
if (result.connect_time !== undefined) {
|
|
91
|
+
point.floatField('connect_time', result.connect_time);
|
|
92
|
+
}
|
|
93
|
+
if (result.latency !== undefined) {
|
|
94
|
+
point.floatField('latency', result.latency);
|
|
95
|
+
}
|
|
96
|
+
if (result.error) {
|
|
97
|
+
point.stringField('error', result.error.substring(0, 255));
|
|
98
|
+
}
|
|
99
|
+
if (result.request_url) {
|
|
100
|
+
point.stringField('request_url', result.request_url.substring(0, 500));
|
|
101
|
+
}
|
|
102
|
+
if (result.request_method) {
|
|
103
|
+
point.tag('method', result.request_method);
|
|
104
|
+
}
|
|
105
|
+
this.writeApi.writePoint(point);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
logger_1.logger.error(`Failed to write test result to InfluxDB: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Write a batch of test results to InfluxDB
|
|
113
|
+
*/
|
|
114
|
+
async writeBatch(results) {
|
|
115
|
+
if (!this.enabled || !this.writeApi)
|
|
116
|
+
return;
|
|
117
|
+
for (const result of results) {
|
|
118
|
+
await this.writeResult(result);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
await this.writeApi.flush();
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
logger_1.logger.error(`Failed to flush test results to InfluxDB: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Write a network call to InfluxDB
|
|
129
|
+
*/
|
|
130
|
+
async writeNetworkCall(call) {
|
|
131
|
+
if (!this.enabled || !this.writeApi)
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
const point = new influxdb_client_1.Point('network_call')
|
|
135
|
+
.tag('test_id', this.currentTestId)
|
|
136
|
+
.tag('test_name', this.currentTestName)
|
|
137
|
+
.tag('method', call.request_method || 'GET')
|
|
138
|
+
.tag('success', call.success ? 'true' : 'false')
|
|
139
|
+
.tag('resource_type', call.resource_type || 'other')
|
|
140
|
+
.intField('vu_id', call.vu_id)
|
|
141
|
+
.floatField('duration', call.duration || 0)
|
|
142
|
+
.intField('status', call.response_status || 0)
|
|
143
|
+
.intField('response_size', call.response_size || 0)
|
|
144
|
+
.stringField('url', (call.request_url || '').substring(0, 500))
|
|
145
|
+
.timestamp(new Date(call.timestamp));
|
|
146
|
+
if (call.scenario) {
|
|
147
|
+
point.tag('scenario', call.scenario);
|
|
148
|
+
}
|
|
149
|
+
if (call.step_name) {
|
|
150
|
+
point.tag('step_name', call.step_name);
|
|
151
|
+
}
|
|
152
|
+
if (call.error) {
|
|
153
|
+
point.stringField('error', call.error.substring(0, 255));
|
|
154
|
+
}
|
|
155
|
+
this.writeApi.writePoint(point);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
logger_1.logger.error(`Failed to write network call to InfluxDB: ${error.message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Write test summary to InfluxDB
|
|
163
|
+
*/
|
|
164
|
+
async writeSummary(summary) {
|
|
165
|
+
if (!this.enabled || !this.writeApi)
|
|
166
|
+
return;
|
|
167
|
+
try {
|
|
168
|
+
const point = new influxdb_client_1.Point('test_summary')
|
|
169
|
+
.tag('test_id', this.currentTestId)
|
|
170
|
+
.tag('test_name', this.currentTestName)
|
|
171
|
+
.intField('total_requests', summary.total_requests)
|
|
172
|
+
.intField('successful_requests', summary.successful_requests)
|
|
173
|
+
.intField('failed_requests', summary.failed_requests)
|
|
174
|
+
.floatField('success_rate', summary.success_rate)
|
|
175
|
+
.floatField('avg_response_time', summary.avg_response_time)
|
|
176
|
+
.floatField('min_response_time', summary.min_response_time)
|
|
177
|
+
.floatField('max_response_time', summary.max_response_time)
|
|
178
|
+
.floatField('p50', summary.percentiles[50] || 0)
|
|
179
|
+
.floatField('p90', summary.percentiles[90] || 0)
|
|
180
|
+
.floatField('p95', summary.percentiles[95] || 0)
|
|
181
|
+
.floatField('p99', summary.percentiles[99] || 0)
|
|
182
|
+
.floatField('requests_per_second', summary.requests_per_second)
|
|
183
|
+
.floatField('total_duration', summary.total_duration)
|
|
184
|
+
.timestamp(new Date());
|
|
185
|
+
this.writeApi.writePoint(point);
|
|
186
|
+
await this.writeApi.flush();
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
logger_1.logger.error(`Failed to write test summary to InfluxDB: ${error.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Query test results from InfluxDB
|
|
194
|
+
*/
|
|
195
|
+
async queryResults(options = {}) {
|
|
196
|
+
if (!this.enabled || !this.queryApi)
|
|
197
|
+
return [];
|
|
198
|
+
try {
|
|
199
|
+
const startTime = options.startTime || new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
200
|
+
const endTime = options.endTime || new Date();
|
|
201
|
+
// Validate time range
|
|
202
|
+
if (startTime.getTime() >= endTime.getTime()) {
|
|
203
|
+
logger_1.logger.debug('Invalid time range for test results query');
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
let query = `from(bucket: "${this.config.bucket}")
|
|
207
|
+
|> range(start: ${startTime.toISOString()}, stop: ${endTime.toISOString()})
|
|
208
|
+
|> filter(fn: (r) => r._measurement == "test_result")`;
|
|
209
|
+
if (options.testId) {
|
|
210
|
+
query += `\n |> filter(fn: (r) => r.test_id == "${options.testId}")`;
|
|
211
|
+
}
|
|
212
|
+
if (options.testName) {
|
|
213
|
+
query += `\n |> filter(fn: (r) => r.test_name == "${options.testName}")`;
|
|
214
|
+
}
|
|
215
|
+
if (options.scenario) {
|
|
216
|
+
query += `\n |> filter(fn: (r) => r.scenario == "${options.scenario}")`;
|
|
217
|
+
}
|
|
218
|
+
query += `\n |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")`;
|
|
219
|
+
if (options.limit) {
|
|
220
|
+
query += `\n |> limit(n: ${options.limit})`;
|
|
221
|
+
}
|
|
222
|
+
const rows = await this.queryApi.collectRows(query);
|
|
223
|
+
return this.rowsToResults(rows);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
logger_1.logger.error(`Failed to query test results from InfluxDB: ${error.message}`);
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Query network calls from InfluxDB
|
|
232
|
+
*/
|
|
233
|
+
async queryNetworkCalls(options = {}) {
|
|
234
|
+
if (!this.enabled || !this.queryApi)
|
|
235
|
+
return [];
|
|
236
|
+
try {
|
|
237
|
+
const startTime = options.startTime || new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
238
|
+
const endTime = options.endTime || new Date();
|
|
239
|
+
// Validate time range
|
|
240
|
+
if (startTime.getTime() >= endTime.getTime()) {
|
|
241
|
+
logger_1.logger.debug('Invalid time range for network calls query');
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
let query = `from(bucket: "${this.config.bucket}")
|
|
245
|
+
|> range(start: ${startTime.toISOString()}, stop: ${endTime.toISOString()})
|
|
246
|
+
|> filter(fn: (r) => r._measurement == "network_call")`;
|
|
247
|
+
if (options.testId) {
|
|
248
|
+
query += `\n |> filter(fn: (r) => r.test_id == "${options.testId}")`;
|
|
249
|
+
}
|
|
250
|
+
if (options.testName) {
|
|
251
|
+
query += `\n |> filter(fn: (r) => r.test_name == "${options.testName}")`;
|
|
252
|
+
}
|
|
253
|
+
query += `\n |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")`;
|
|
254
|
+
if (options.limit) {
|
|
255
|
+
query += `\n |> limit(n: ${options.limit})`;
|
|
256
|
+
}
|
|
257
|
+
const rows = await this.queryApi.collectRows(query);
|
|
258
|
+
return this.rowsToNetworkCalls(rows);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
logger_1.logger.error(`Failed to query network calls from InfluxDB: ${error.message}`);
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get list of test runs
|
|
267
|
+
*/
|
|
268
|
+
async getTestRuns(limit = 100) {
|
|
269
|
+
if (!this.enabled || !this.queryApi)
|
|
270
|
+
return [];
|
|
271
|
+
try {
|
|
272
|
+
const query = `from(bucket: "${this.config.bucket}")
|
|
273
|
+
|> range(start: -30d)
|
|
274
|
+
|> filter(fn: (r) => r._measurement == "test_summary")
|
|
275
|
+
|> keep(columns: ["test_id", "test_name", "_time"])
|
|
276
|
+
|> sort(columns: ["_time"], desc: true)
|
|
277
|
+
|> limit(n: ${limit})`;
|
|
278
|
+
const rows = await this.queryApi.collectRows(query);
|
|
279
|
+
return rows.map((row) => ({
|
|
280
|
+
testId: row.test_id,
|
|
281
|
+
testName: row.test_name,
|
|
282
|
+
timestamp: new Date(row._time)
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
logger_1.logger.error(`Failed to get test runs from InfluxDB: ${error.message}`);
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Export test data for a specific test run
|
|
292
|
+
*/
|
|
293
|
+
async exportTestData(testId, format = 'json') {
|
|
294
|
+
const results = await this.queryResults({ testId });
|
|
295
|
+
const networkCalls = await this.queryNetworkCalls({ testId });
|
|
296
|
+
const data = {
|
|
297
|
+
testId,
|
|
298
|
+
results,
|
|
299
|
+
networkCalls,
|
|
300
|
+
exportedAt: new Date().toISOString()
|
|
301
|
+
};
|
|
302
|
+
if (format === 'csv') {
|
|
303
|
+
return this.resultsToCSV(results);
|
|
304
|
+
}
|
|
305
|
+
return JSON.stringify(data, null, 2);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Finalize the current test - flush all pending writes
|
|
309
|
+
*/
|
|
310
|
+
async finalize() {
|
|
311
|
+
if (this.writeApi) {
|
|
312
|
+
try {
|
|
313
|
+
await this.writeApi.flush();
|
|
314
|
+
logger_1.logger.debug('InfluxDB writer finalized for test: ' + this.currentTestName);
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
logger_1.logger.error(`Failed to finalize InfluxDB writes: ${error.message}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Close the connection
|
|
323
|
+
*/
|
|
324
|
+
async close() {
|
|
325
|
+
if (this.writeApi) {
|
|
326
|
+
await this.writeApi.close();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
rowsToResults(rows) {
|
|
330
|
+
return rows.map(row => ({
|
|
331
|
+
id: `${row.test_id}-${row._time}`,
|
|
332
|
+
vu_id: row.vu_id || 0,
|
|
333
|
+
iteration: row.iteration || 0,
|
|
334
|
+
scenario: row.scenario || 'default',
|
|
335
|
+
action: row.action || 'unknown',
|
|
336
|
+
timestamp: new Date(row._time).getTime(),
|
|
337
|
+
duration: row.duration || 0,
|
|
338
|
+
success: row.success === 'true',
|
|
339
|
+
status: row.status || 0,
|
|
340
|
+
response_size: row.response_size,
|
|
341
|
+
connect_time: row.connect_time,
|
|
342
|
+
latency: row.latency,
|
|
343
|
+
error: row.error,
|
|
344
|
+
request_url: row.request_url,
|
|
345
|
+
request_method: row.method
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
rowsToNetworkCalls(rows) {
|
|
349
|
+
return rows.map(row => ({
|
|
350
|
+
id: `${row.test_id}-${row._time}`,
|
|
351
|
+
vu_id: row.vu_id || 0,
|
|
352
|
+
timestamp: new Date(row._time).getTime(),
|
|
353
|
+
request_url: row.url || '',
|
|
354
|
+
request_method: row.method || 'GET',
|
|
355
|
+
response_status: row.status || 0,
|
|
356
|
+
response_size: row.response_size || 0,
|
|
357
|
+
start_time: new Date(row._time).getTime(),
|
|
358
|
+
duration: row.duration || 0,
|
|
359
|
+
resource_type: row.resource_type || 'other',
|
|
360
|
+
success: row.success === 'true',
|
|
361
|
+
error: row.error,
|
|
362
|
+
scenario: row.scenario,
|
|
363
|
+
step_name: row.step_name
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
resultsToCSV(results) {
|
|
367
|
+
const headers = [
|
|
368
|
+
'timestamp', 'vu_id', 'iteration', 'scenario', 'action', 'success',
|
|
369
|
+
'duration', 'status', 'response_size', 'connect_time', 'latency',
|
|
370
|
+
'request_url', 'request_method', 'error'
|
|
371
|
+
];
|
|
372
|
+
const rows = results.map(r => [
|
|
373
|
+
new Date(r.timestamp).toISOString(),
|
|
374
|
+
r.vu_id,
|
|
375
|
+
r.iteration || 0,
|
|
376
|
+
r.scenario,
|
|
377
|
+
r.action || '',
|
|
378
|
+
r.success,
|
|
379
|
+
r.duration || 0,
|
|
380
|
+
r.status || 0,
|
|
381
|
+
r.response_size || 0,
|
|
382
|
+
r.connect_time || 0,
|
|
383
|
+
r.latency || 0,
|
|
384
|
+
(r.request_url || '').replace(/,/g, ';'),
|
|
385
|
+
r.request_method || '',
|
|
386
|
+
(r.error || '').replace(/,/g, ';').replace(/\n/g, ' ')
|
|
387
|
+
].join(','));
|
|
388
|
+
return [headers.join(','), ...rows].join('\n');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
exports.InfluxDBWriter = InfluxDBWriter;
|
|
392
|
+
// Singleton instance for global access
|
|
393
|
+
let influxDBWriter = null;
|
|
394
|
+
function getInfluxDBWriter() {
|
|
395
|
+
return influxDBWriter;
|
|
396
|
+
}
|
|
397
|
+
function setInfluxDBWriter(writer) {
|
|
398
|
+
influxDBWriter = writer;
|
|
399
|
+
}
|
|
400
|
+
async function initInfluxDBWriter(config) {
|
|
401
|
+
influxDBWriter = new InfluxDBWriter(config);
|
|
402
|
+
await influxDBWriter.connect();
|
|
403
|
+
return influxDBWriter;
|
|
404
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TestResult } from '../types';
|
|
2
|
+
export interface RealtimeEndpoint {
|
|
3
|
+
type: 'graphite' | 'webhook' | 'influxdb' | 'websocket';
|
|
4
|
+
url?: string;
|
|
5
|
+
host?: string;
|
|
6
|
+
port?: number;
|
|
7
|
+
database?: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export declare class RealtimeDispatcher {
|
|
12
|
+
private endpoints;
|
|
13
|
+
private startTime;
|
|
14
|
+
setEndpoints(endpoints: RealtimeEndpoint[]): void;
|
|
15
|
+
setStartTime(time: number): void;
|
|
16
|
+
dispatch(batch: TestResult[], batchNumber: number): Promise<void>;
|
|
17
|
+
private sendToEndpoint;
|
|
18
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RealtimeDispatcher = void 0;
|
|
4
|
+
const logger_1 = require("../../utils/logger");
|
|
5
|
+
const graphite_1 = require("./endpoints/graphite");
|
|
6
|
+
const webhook_1 = require("./endpoints/webhook");
|
|
7
|
+
const influxdb_1 = require("./endpoints/influxdb");
|
|
8
|
+
const websocket_1 = require("./endpoints/websocket");
|
|
9
|
+
class RealtimeDispatcher {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.endpoints = [];
|
|
12
|
+
this.startTime = 0;
|
|
13
|
+
}
|
|
14
|
+
setEndpoints(endpoints) {
|
|
15
|
+
this.endpoints = endpoints;
|
|
16
|
+
}
|
|
17
|
+
setStartTime(time) {
|
|
18
|
+
this.startTime = time;
|
|
19
|
+
}
|
|
20
|
+
async dispatch(batch, batchNumber) {
|
|
21
|
+
if (this.endpoints.length === 0)
|
|
22
|
+
return;
|
|
23
|
+
const promises = this.endpoints.map(endpoint => this.sendToEndpoint(batch, endpoint, batchNumber).catch(error => logger_1.logger.warn(`Failed to send to ${endpoint.type} endpoint:`, error)));
|
|
24
|
+
await Promise.allSettled(promises);
|
|
25
|
+
}
|
|
26
|
+
async sendToEndpoint(batch, endpoint, batchNumber) {
|
|
27
|
+
switch (endpoint.type) {
|
|
28
|
+
case 'graphite':
|
|
29
|
+
await (0, graphite_1.sendToGraphite)(batch, endpoint);
|
|
30
|
+
break;
|
|
31
|
+
case 'webhook':
|
|
32
|
+
await (0, webhook_1.sendToWebhook)(batch, endpoint, batchNumber, this.startTime);
|
|
33
|
+
break;
|
|
34
|
+
case 'influxdb':
|
|
35
|
+
await (0, influxdb_1.sendToInfluxDB)(batch, endpoint, batchNumber);
|
|
36
|
+
break;
|
|
37
|
+
case 'websocket':
|
|
38
|
+
await (0, websocket_1.sendToWebSocket)(batch, endpoint, batchNumber, this.startTime);
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
logger_1.logger.warn(`Unknown endpoint type: ${endpoint.type}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.RealtimeDispatcher = RealtimeDispatcher;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.sendToGraphite = sendToGraphite;
|
|
37
|
+
const net = __importStar(require("net"));
|
|
38
|
+
async function sendToGraphite(batch, config) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const client = net.createConnection(config.port, config.host);
|
|
41
|
+
client.on('connect', () => {
|
|
42
|
+
const metrics = batch.map(result => {
|
|
43
|
+
const timestamp = Math.floor(result.timestamp / 1000);
|
|
44
|
+
const metricName = `loadtest.${result.scenario}.${result.step_name || result.action}`;
|
|
45
|
+
return [
|
|
46
|
+
`${metricName}.duration ${result.duration} ${timestamp}`,
|
|
47
|
+
`${metricName}.success ${result.success ? 1 : 0} ${timestamp}`,
|
|
48
|
+
`${metricName}.count 1 ${timestamp}`
|
|
49
|
+
].join('\n');
|
|
50
|
+
}).join('\n') + '\n';
|
|
51
|
+
client.write(metrics);
|
|
52
|
+
client.end();
|
|
53
|
+
});
|
|
54
|
+
client.on('close', () => resolve());
|
|
55
|
+
client.on('error', reject);
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
client.destroy();
|
|
58
|
+
reject(new Error('Graphite connection timeout'));
|
|
59
|
+
}, 5000);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sendToInfluxDB = sendToInfluxDB;
|
|
4
|
+
async function sendToInfluxDB(batch, config, batchNumber) {
|
|
5
|
+
const lines = batch.map(result => {
|
|
6
|
+
const tags = [
|
|
7
|
+
`scenario=${result.scenario}`,
|
|
8
|
+
`step=${result.step_name || result.action}`,
|
|
9
|
+
`vu_id=${result.vu_id}`,
|
|
10
|
+
`success=${result.success}`
|
|
11
|
+
].join(',');
|
|
12
|
+
const fields = [
|
|
13
|
+
`duration=${result.duration}`,
|
|
14
|
+
`success=${result.success ? 'true' : 'false'}`,
|
|
15
|
+
`batch_number=${batchNumber}i`
|
|
16
|
+
];
|
|
17
|
+
if (result.status) {
|
|
18
|
+
fields.push(`status=${result.status}i`);
|
|
19
|
+
}
|
|
20
|
+
const timestamp = result.timestamp * 1000000; // Convert to nanoseconds
|
|
21
|
+
return `loadtest,${tags} ${fields.join(',')} ${timestamp}`;
|
|
22
|
+
}).join('\n');
|
|
23
|
+
const response = await fetch(`${config.url}/write?db=${config.database}`, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Authorization': `Bearer ${config.token}`,
|
|
27
|
+
'Content-Type': 'text/plain'
|
|
28
|
+
},
|
|
29
|
+
body: lines
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const errorText = await response.text();
|
|
33
|
+
throw new Error(`InfluxDB write failed: ${response.status} ${errorText}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sendToWebhook = sendToWebhook;
|
|
4
|
+
async function sendToWebhook(batch, config, batchNumber, startTime) {
|
|
5
|
+
const response = await fetch(config.url, {
|
|
6
|
+
method: 'POST',
|
|
7
|
+
headers: {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
...config.headers
|
|
10
|
+
},
|
|
11
|
+
body: JSON.stringify({
|
|
12
|
+
timestamp: new Date().toISOString(),
|
|
13
|
+
batch_number: batchNumber,
|
|
14
|
+
batch_size: batch.length,
|
|
15
|
+
test_start_time: new Date(startTime).toISOString(),
|
|
16
|
+
results: batch
|
|
17
|
+
})
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(`Webhook failed: ${response.status} ${response.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sendToWebSocket = sendToWebSocket;
|
|
4
|
+
const ws_1 = require("ws");
|
|
5
|
+
async function sendToWebSocket(batch, config, batchNumber, startTime) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const ws = new ws_1.WebSocket(config.url);
|
|
8
|
+
ws.on('open', () => {
|
|
9
|
+
ws.send(JSON.stringify({
|
|
10
|
+
type: 'metrics_batch',
|
|
11
|
+
timestamp: new Date().toISOString(),
|
|
12
|
+
batch_number: batchNumber,
|
|
13
|
+
test_start_time: new Date(startTime).toISOString(),
|
|
14
|
+
data: batch
|
|
15
|
+
}));
|
|
16
|
+
ws.close();
|
|
17
|
+
resolve();
|
|
18
|
+
});
|
|
19
|
+
ws.on('error', reject);
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
ws.close();
|
|
22
|
+
reject(new Error('WebSocket connection timeout'));
|
|
23
|
+
}, 5000);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { RealtimeDispatcher, RealtimeEndpoint } from './dispatcher';
|
|
2
|
+
export { sendToGraphite } from './endpoints/graphite';
|
|
3
|
+
export { sendToWebhook } from './endpoints/webhook';
|
|
4
|
+
export { sendToInfluxDB } from './endpoints/influxdb';
|
|
5
|
+
export { sendToWebSocket } from './endpoints/websocket';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sendToWebSocket = exports.sendToInfluxDB = exports.sendToWebhook = exports.sendToGraphite = exports.RealtimeDispatcher = void 0;
|
|
4
|
+
var dispatcher_1 = require("./dispatcher");
|
|
5
|
+
Object.defineProperty(exports, "RealtimeDispatcher", { enumerable: true, get: function () { return dispatcher_1.RealtimeDispatcher; } });
|
|
6
|
+
var graphite_1 = require("./endpoints/graphite");
|
|
7
|
+
Object.defineProperty(exports, "sendToGraphite", { enumerable: true, get: function () { return graphite_1.sendToGraphite; } });
|
|
8
|
+
var webhook_1 = require("./endpoints/webhook");
|
|
9
|
+
Object.defineProperty(exports, "sendToWebhook", { enumerable: true, get: function () { return webhook_1.sendToWebhook; } });
|
|
10
|
+
var influxdb_1 = require("./endpoints/influxdb");
|
|
11
|
+
Object.defineProperty(exports, "sendToInfluxDB", { enumerable: true, get: function () { return influxdb_1.sendToInfluxDB; } });
|
|
12
|
+
var websocket_1 = require("./endpoints/websocket");
|
|
13
|
+
Object.defineProperty(exports, "sendToWebSocket", { enumerable: true, get: function () { return websocket_1.sendToWebSocket; } });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TimelineCalculator = exports.StepStatisticsCalculator = exports.SummaryGenerator = void 0;
|
|
4
|
+
var summary_generator_1 = require("./summary-generator");
|
|
5
|
+
Object.defineProperty(exports, "SummaryGenerator", { enumerable: true, get: function () { return summary_generator_1.SummaryGenerator; } });
|
|
6
|
+
var step_statistics_1 = require("./step-statistics");
|
|
7
|
+
Object.defineProperty(exports, "StepStatisticsCalculator", { enumerable: true, get: function () { return step_statistics_1.StepStatisticsCalculator; } });
|
|
8
|
+
var timeline_calculator_1 = require("./timeline-calculator");
|
|
9
|
+
Object.defineProperty(exports, "TimelineCalculator", { enumerable: true, get: function () { return timeline_calculator_1.TimelineCalculator; } });
|