@contrail/flexplm 1.8.1 → 2.0.0-alpha.df05bb5

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 (34) hide show
  1. package/lib/__tests__/telemetry-test-utils.d.ts +8 -0
  2. package/lib/__tests__/telemetry-test-utils.js +30 -0
  3. package/lib/cli/commands/compile.js +17 -7
  4. package/lib/cli/commands/compile.spec.js +17 -7
  5. package/lib/cli/commands/create.js +17 -7
  6. package/lib/cli/commands/create.spec.js +17 -7
  7. package/lib/cli/commands/upload.js +17 -7
  8. package/lib/cli/index.js +2 -2
  9. package/lib/entity-processor/base-entity-processor.d.ts +29 -0
  10. package/lib/entity-processor/base-entity-processor.js +238 -143
  11. package/lib/entity-processor/base-entity-processor.spec.js +210 -0
  12. package/lib/index.d.ts +1 -0
  13. package/lib/index.js +1 -0
  14. package/lib/publish/base-process-publish-assortment-callback.js +30 -0
  15. package/lib/publish/base-process-publish-assortment-callback.spec.d.ts +1 -0
  16. package/lib/publish/base-process-publish-assortment-callback.spec.js +62 -0
  17. package/lib/publish/base-process-publish-assortment.d.ts +24 -0
  18. package/lib/publish/base-process-publish-assortment.js +234 -132
  19. package/lib/publish/base-process-publish-assortment.spec.js +188 -0
  20. package/lib/util/error-response-object.js +2 -0
  21. package/lib/util/event-short-message-status.js +1 -1
  22. package/lib/util/flexplm-connect.d.ts +4 -0
  23. package/lib/util/flexplm-connect.js +210 -145
  24. package/lib/util/flexplm-connect.telemetry.spec.d.ts +1 -0
  25. package/lib/util/flexplm-connect.telemetry.spec.js +212 -0
  26. package/lib/util/logger-config.js +1 -2
  27. package/lib/util/telemetry-attributes.d.ts +68 -0
  28. package/lib/util/telemetry-attributes.js +73 -0
  29. package/lib/util/telemetry-utils.d.ts +3 -0
  30. package/lib/util/telemetry-utils.js +10 -0
  31. package/lib/util/thumbnail-util.d.ts +2 -1
  32. package/lib/util/thumbnail-util.js +35 -7
  33. package/lib/util/thumbnail-util.spec.js +42 -0
  34. package/package.json +10 -3
@@ -2,6 +2,25 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FlexPLMConnect = void 0;
4
4
  const app_framework_1 = require("@contrail/app-framework");
5
+ const telemetry_1 = require("@contrail/telemetry");
6
+ const telemetry_attributes_1 = require("./telemetry-attributes");
7
+ const telemetry_utils_1 = require("./telemetry-utils");
8
+ // Telemetry: only the path is recorded, never the host or the query string (it can carry file paths).
9
+ const toUrlPath = (path) => (path || '').split('?')[0];
10
+ const MAX_LOGGED_RESPONSE_BODY = 2048;
11
+ /** Reads the response body for an error log; never throws (the body may already be consumed). */
12
+ const readBodyForLog = async (response) => {
13
+ try {
14
+ const text = await response.text();
15
+ return text.length > MAX_LOGGED_RESPONSE_BODY ? text.substring(0, MAX_LOGGED_RESPONSE_BODY) + '...(truncated)' : text;
16
+ }
17
+ catch (e) {
18
+ return '(response body unavailable)';
19
+ }
20
+ };
21
+ const logFlexPLMError = async (message, response, urlPath) => {
22
+ telemetry_1.logger.error({ flexplm: { status: response.status, url_path: urlPath, response_body: await readBodyForLog(response) } }, message);
23
+ };
5
24
  // Module-level on purpose: a new FlexPLMConnect is constructed for every event. Declaring for Session reusing use case.
6
25
  const sessionCache = new Map();
7
26
  // Upper bound so users/option combinations that are never requested again cannot accumulate cookies forever.
@@ -98,6 +117,14 @@ class FlexPLMConnect {
98
117
  }
99
118
  })();
100
119
  }
120
+ /** Marks the active span when this request is about to wait for another request's cold acquisition.
121
+ * Synchronous on purpose: it must not add an await to the wait loops (see waitForInFlightAcquisition).
122
+ */
123
+ recordWaitForInFlightAcquisition() {
124
+ if (inFlightAcquisitions.has(this.getSessionKey())) {
125
+ (0, telemetry_utils_1.setActiveSpanAttributes)({ [telemetry_attributes_1.ATTR_FLEXPLM_SESSION_WAITED]: true });
126
+ }
127
+ }
101
128
  /** Runs a cold acquisition and registers it so concurrent callers wait for it instead of opening their own session. */
102
129
  async runColdAcquisition(acquire) {
103
130
  const key = this.getSessionKey();
@@ -223,8 +250,11 @@ class FlexPLMConnect {
223
250
  // rejectedCookie: the cookie that was rejected by the caller, so a refresh already done by a concurrent event is not thrown away
224
251
  async getCSRF(getNewSession = false, rejectedCookie) {
225
252
  // Added for Session reuse - ET-390 - Starts
226
- if (!this.sessionOptions.enabled)
253
+ if (!this.sessionOptions.enabled) {
254
+ (0, telemetry_utils_1.setActiveSpanAttributes)({ [telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]: 'disabled' });
227
255
  return this.fetchCSRF(undefined);
256
+ }
257
+ this.recordWaitForInFlightAcquisition();
228
258
  for (let wait = this.waitForInFlightAcquisition(); wait; wait = this.waitForInFlightAcquisition())
229
259
  await wait;
230
260
  if (getNewSession) {
@@ -233,6 +263,8 @@ class FlexPLMConnect {
233
263
  this.clearSession();
234
264
  }
235
265
  const cached = this.getCachedSession();
266
+ // Set on the caller's span; a retry below overwrites it with the session that was finally used.
267
+ (0, telemetry_utils_1.setActiveSpanAttributes)({ [telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]: cached ? 'cached' : 'new' });
236
268
  if (cached) {
237
269
  try {
238
270
  return await this.fetchCSRF(cached);
@@ -254,50 +286,62 @@ class FlexPLMConnect {
254
286
  const csrfEndpoint = this.config.csrfEndpoint;
255
287
  const csrfURL = this.config.apiHost + urlContext + csrfEndpoint;
256
288
  console.info('csrfURL: ' + csrfURL);
257
- // getRequestOptions will be called with additional sentCookie parameter
258
- const csrfOptions = this.getRequestOptions('GET', sentCookie);
259
- const response = await fetch(csrfURL, csrfOptions);
260
- // Added for Session reuse - ET-390 - Starts
261
- if (sentCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
262
- console.warn('Cached FlexPLM session rejected on csrf call (status ' + response.status + '). Retrying with a new session.');
263
- await this.discardBody(response);
289
+ const urlPath = toUrlPath(urlContext + csrfEndpoint);
290
+ const result = await (0, telemetry_1.traceSection)('fetch_flexplm_csrf', {
291
+ [telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_KIND]: 'csrf',
292
+ [telemetry_attributes_1.ATTR_HTTP_REQUEST_METHOD]: 'GET',
293
+ [telemetry_attributes_1.ATTR_URL_PATH]: urlPath,
294
+ }, async (span) => {
295
+ // getRequestOptions will be called with additional sentCookie parameter
296
+ const csrfOptions = this.getRequestOptions('GET', sentCookie);
297
+ const response = await fetch(csrfURL, csrfOptions);
298
+ span.setAttribute(telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
299
+ // Added for Session reuse - ET-390 - Starts
300
+ if (sentCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
301
+ console.warn('Cached FlexPLM session rejected on csrf call (status ' + response.status + '). Retrying with a new session.');
302
+ // Expected and retried by getCSRF, so it is an event and the span stays OK.
303
+ span.addEvent('flexplm_session_rejected');
304
+ await this.discardBody(response);
305
+ return { sessionRejected: true };
306
+ }
307
+ // Added for Session reuse - ET-390 - Ends
308
+ if (response.status >= 300) {
309
+ const message = 'Error connecting to FlexPLM:status: ' + response.status;
310
+ await logFlexPLMError(message, response, urlPath);
311
+ const e = new Error(message);
312
+ e['httpResponseStatus'] = response.status;
313
+ throw e;
314
+ }
315
+ // Added for Session reuse - ET-390 - Starts
316
+ // If the cookie we sent was invalid, Windchill answers with a fresh Set-Cookie --> replace the cache.
317
+ let sessionCookie = sentCookie;
318
+ if (this.sessionOptions.enabled) {
319
+ sessionCookie = this.extractSessionCookie(response) ?? sentCookie;
320
+ if (sessionCookie)
321
+ this.storeSession(sessionCookie);
322
+ }
323
+ // Added for Session reuse - ET-390 - Ends
324
+ try {
325
+ const body = await response.json();
326
+ const nonce_key = body.items[0].attributes.nonce_key;
327
+ const nonce = body.items[0].attributes.nonce;
328
+ // ET-390 - adding sessionCookie to the return
329
+ return {
330
+ nonce_key,
331
+ nonce,
332
+ sessionCookie,
333
+ };
334
+ }
335
+ catch (e) {
336
+ const message = 'Error connecting to FlexPLM: ' + e.message;
337
+ await logFlexPLMError(message, response, urlPath);
338
+ throw new Error(message);
339
+ }
340
+ });
341
+ // Thrown outside the span so the expected rejection is not recorded as a span error.
342
+ if ('sessionRejected' in result)
264
343
  throw new SessionRejectedError();
265
- }
266
- // Added for Session reuse - ET-390 - Ends
267
- if (response.status >= 300) {
268
- const message = 'Error connecting to FlexPLM:status: ' + response.status;
269
- console.error(message);
270
- console.error(await response.text());
271
- const e = new Error(message);
272
- e['httpResponseStatus'] = response.status;
273
- throw e;
274
- }
275
- // Added for Session reuse - ET-390 - Starts
276
- // If the cookie we sent was invalid, Windchill answers with a fresh Set-Cookie --> replace the cache.
277
- let sessionCookie = sentCookie;
278
- if (this.sessionOptions.enabled) {
279
- sessionCookie = this.extractSessionCookie(response) ?? sentCookie;
280
- if (sessionCookie)
281
- this.storeSession(sessionCookie);
282
- }
283
- // Added for Session reuse - ET-390 - Ends
284
- try {
285
- const body = await response.json();
286
- const nonce_key = body.items[0].attributes.nonce_key;
287
- const nonce = body.items[0].attributes.nonce;
288
- // ET-390 - adding sessionCookie to the return
289
- return {
290
- nonce_key,
291
- nonce,
292
- sessionCookie,
293
- };
294
- }
295
- catch (e) {
296
- const message = 'Error connecting to FlexPLM: ' + e.message;
297
- console.error(message);
298
- console.error(await response.text());
299
- throw new Error(message);
300
- }
344
+ return result;
301
345
  }
302
346
  async sendRequest(csrf, payload) {
303
347
  if (this.payloadSendAsArray && !Array.isArray(payload)) {
@@ -325,63 +369,73 @@ class FlexPLMConnect {
325
369
  return eventResponse;
326
370
  }
327
371
  async processRequest(payload) {
328
- if (!payload) {
329
- const message = 'No payload to send to FlexPLM';
330
- console.error(message);
331
- throw new Error(message);
332
- }
333
- if (app_framework_1.Logger.isInfoOn()) {
334
- console.info('payload: ' + JSON.stringify(payload));
335
- }
336
- let csrf = await this.getCSRF();
337
- if (!csrf) {
338
- const message = 'Failed to get CSRF nonce';
339
- console.error(message);
340
- throw new Error(message);
341
- }
342
- /// Added for Session reuse - ET-390 - Starts
343
- // const response = await this.sendRequest(csrf, payload);
344
- let response = await this.sendRequest(csrf, payload);
345
- if (csrf.sessionCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
346
- console.warn('FlexPLM rejected session/nonce (status ' + response.status + '). Getting a new session and retrying once.');
347
- await this.discardBody(response);
348
- csrf = await this.getCSRF(true, csrf.sessionCookie);
349
- response = await this.sendRequest(csrf, payload);
350
- }
351
- // Added for Session reuse - ET-390 - Ends
352
- const status = response.status;
353
- if (status >= 300) {
354
- const message = 'Error sending data to FlexPLM:status: ' + response.status;
355
- console.error(message);
356
- console.error(await response.text());
357
- const e = new Error(message);
358
- e['httpResponseStatus'] = status;
359
- throw e;
360
- }
361
- try {
362
- const res = {
363
- status,
364
- };
365
- if (![204, 205].includes(status)) {
366
- try {
367
- const data = await response.json();
368
- res.data = data;
369
- }
370
- catch (e) {
371
- console.error('Error getting response body. Setting {} body: ' + e);
372
- res.data = {};
372
+ const urlPath = toUrlPath(this.config.urlContext + '/servlet/rest' + this.vibeEventEndpoint);
373
+ return (0, telemetry_1.traceSection)('send_to_flexplm', {
374
+ [telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_KIND]: 'send',
375
+ [telemetry_attributes_1.ATTR_HTTP_REQUEST_METHOD]: 'POST',
376
+ [telemetry_attributes_1.ATTR_URL_PATH]: urlPath,
377
+ [telemetry_attributes_1.ATTR_FLEXPLM_PAYLOAD_COUNT]: Array.isArray(payload) ? payload.length : (payload ? 1 : 0),
378
+ [telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED]: false,
379
+ }, async (span) => {
380
+ if (!payload) {
381
+ const message = 'No payload to send to FlexPLM';
382
+ console.error(message);
383
+ throw new Error(message);
384
+ }
385
+ if (app_framework_1.Logger.isInfoOn()) {
386
+ console.info('payload: ' + JSON.stringify(payload));
387
+ }
388
+ let csrf = await this.getCSRF();
389
+ if (!csrf) {
390
+ const message = 'Failed to get CSRF nonce';
391
+ console.error(message);
392
+ throw new Error(message);
393
+ }
394
+ /// Added for Session reuse - ET-390 - Starts
395
+ // const response = await this.sendRequest(csrf, payload);
396
+ let response = await this.sendRequest(csrf, payload);
397
+ if (csrf.sessionCookie && this.sessionOptions.retryStatuses.includes(response.status)) {
398
+ console.warn('FlexPLM rejected session/nonce (status ' + response.status + '). Getting a new session and retrying once.');
399
+ span.setAttribute(telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED, true);
400
+ await this.discardBody(response);
401
+ csrf = await this.getCSRF(true, csrf.sessionCookie);
402
+ response = await this.sendRequest(csrf, payload);
403
+ }
404
+ // Added for Session reuse - ET-390 - Ends
405
+ const status = response.status;
406
+ span.setAttribute(telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE, status);
407
+ if (status >= 300) {
408
+ // traceSection records the thrown error and sets the span status to ERROR.
409
+ const message = 'Error sending data to FlexPLM:status: ' + response.status;
410
+ await logFlexPLMError(message, response, urlPath);
411
+ const e = new Error(message);
412
+ e['httpResponseStatus'] = status;
413
+ throw e;
414
+ }
415
+ try {
416
+ const res = {
417
+ status,
418
+ };
419
+ if (![204, 205].includes(status)) {
420
+ try {
421
+ const data = await response.json();
422
+ res.data = data;
423
+ }
424
+ catch (e) {
425
+ console.error('Error getting response body. Setting {} body: ' + e);
426
+ res.data = {};
427
+ }
373
428
  }
429
+ console.log('eventResponse.status: ' + status);
430
+ // console.log('eventBody: ', JSON.stringify(data));
431
+ return res;
374
432
  }
375
- console.log('eventResponse.status: ' + status);
376
- // console.log('eventBody: ', JSON.stringify(data));
377
- return res;
378
- }
379
- catch (e) {
380
- const message = 'Error getting json data from FlexPLM: ' + e.message;
381
- console.error(message);
382
- console.error(await response.text());
383
- throw new Error(message);
384
- }
433
+ catch (e) {
434
+ const message = 'Error getting json data from FlexPLM: ' + e.message;
435
+ await logFlexPLMError(message, response, urlPath);
436
+ throw new Error(message);
437
+ }
438
+ });
385
439
  }
386
440
  async sendToFlexPLM(payload) {
387
441
  return await this.processRequest(payload);
@@ -401,54 +455,65 @@ class FlexPLMConnect {
401
455
  const urlContext = includeUrlContext ? this.config.urlContext : '';
402
456
  const path = urlPath || ('/servlet/rest' + this.vibeEventEndpoint);
403
457
  const vibeEventsURL = this.config.apiHost + urlContext + path;
404
- // Added for Session reuse - ET-390 - Starts
405
- const attempt = async () => {
406
- if (!this.sessionOptions.enabled) {
407
- return { response: await fetch(vibeEventsURL, this.getRequestOptions('GET')), sent: undefined };
408
- }
409
- for (let wait = this.waitForInFlightAcquisition(); wait; wait = this.waitForInFlightAcquisition())
410
- await wait;
411
- const sent = this.getCachedSession();
412
- const call = async () => {
413
- const res = await fetch(vibeEventsURL, this.getRequestOptions('GET', sent));
414
- // A rejected cookie must not be stored again; only keep a cookie that was accepted or newly issued.
415
- if (!(sent && this.sessionOptions.retryStatuses.includes(res.status))) {
416
- const cookie = this.extractSessionCookie(res) ?? sent;
417
- if (cookie)
418
- this.storeSession(cookie);
458
+ const spanUrlPath = toUrlPath(urlContext + path);
459
+ return (0, telemetry_1.traceSection)('get_from_flexplm', {
460
+ [telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_KIND]: 'get',
461
+ [telemetry_attributes_1.ATTR_HTTP_REQUEST_METHOD]: 'GET',
462
+ [telemetry_attributes_1.ATTR_URL_PATH]: spanUrlPath,
463
+ [telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED]: false,
464
+ }, async (span) => {
465
+ // Added for Session reuse - ET-390 - Starts
466
+ const attempt = async () => {
467
+ if (!this.sessionOptions.enabled) {
468
+ span.setAttribute(telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE, 'disabled');
469
+ return { response: await fetch(vibeEventsURL, this.getRequestOptions('GET')), sent: undefined };
419
470
  }
420
- return { response: res, sent };
471
+ this.recordWaitForInFlightAcquisition();
472
+ for (let wait = this.waitForInFlightAcquisition(); wait; wait = this.waitForInFlightAcquisition())
473
+ await wait;
474
+ const sent = this.getCachedSession();
475
+ span.setAttribute(telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE, sent ? 'cached' : 'new');
476
+ const call = async () => {
477
+ const res = await fetch(vibeEventsURL, this.getRequestOptions('GET', sent));
478
+ // A rejected cookie must not be stored again; only keep a cookie that was accepted or newly issued.
479
+ if (!(sent && this.sessionOptions.retryStatuses.includes(res.status))) {
480
+ const cookie = this.extractSessionCookie(res) ?? sent;
481
+ if (cookie)
482
+ this.storeSession(cookie);
483
+ }
484
+ return { response: res, sent };
485
+ };
486
+ return sent ? call() : this.runColdAcquisition(call);
421
487
  };
422
- return sent ? call() : this.runColdAcquisition(call);
423
- };
424
- const first = await attempt();
425
- let response = first.response;
426
- if (first.sent && this.sessionOptions.retryStatuses.includes(response.status)) {
427
- await this.discardBody(response);
428
- if (this.getCachedSession() === first.sent)
429
- this.clearSession();
430
- response = (await attempt()).response;
431
- }
432
- // Added for Session reuse - ET-390 - Ends
433
- if (response.status >= 300) {
434
- const message = 'Error connecting to FlexPLM:status: ' + response.status;
435
- console.error(message);
436
- console.error(await response.text());
437
- throw new Error(message);
438
- }
439
- if (returnFullResponse) {
440
- return response;
441
- }
442
- try {
443
- const data = await response.json();
444
- return data;
445
- }
446
- catch (e) {
447
- const message = 'Error getting json data from FlexPLM: ' + e.message;
448
- console.error(message);
449
- console.error(await response.text());
450
- throw new Error(message);
451
- }
488
+ const first = await attempt();
489
+ let response = first.response;
490
+ if (first.sent && this.sessionOptions.retryStatuses.includes(response.status)) {
491
+ span.setAttribute(telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED, true);
492
+ await this.discardBody(response);
493
+ if (this.getCachedSession() === first.sent)
494
+ this.clearSession();
495
+ response = (await attempt()).response;
496
+ }
497
+ // Added for Session reuse - ET-390 - Ends
498
+ span.setAttribute(telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
499
+ if (response.status >= 300) {
500
+ const message = 'Error connecting to FlexPLM:status: ' + response.status;
501
+ await logFlexPLMError(message, response, spanUrlPath);
502
+ throw new Error(message);
503
+ }
504
+ if (returnFullResponse) {
505
+ return response;
506
+ }
507
+ try {
508
+ const data = await response.json();
509
+ return data;
510
+ }
511
+ catch (e) {
512
+ const message = 'Error getting json data from FlexPLM: ' + e.message;
513
+ await logFlexPLMError(message, response, spanUrlPath);
514
+ throw new Error(message);
515
+ }
516
+ });
452
517
  }
453
518
  }
454
519
  exports.FlexPLMConnect = FlexPLMConnect;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const api_1 = require("@opentelemetry/api");
4
+ const telemetry_1 = require("@contrail/telemetry");
5
+ const flexplm_connect_1 = require("./flexplm-connect");
6
+ const telemetry_test_utils_1 = require("../__tests__/telemetry-test-utils");
7
+ const telemetry_attributes_1 = require("./telemetry-attributes");
8
+ // Same mocks as flexplm-connect.spec.ts
9
+ const csrfBody = { items: [{ attributes: { nonce_key: 'CSRF_NONCE', nonce: 'secret-nonce-value' } }] };
10
+ const resp = (status, setCookie = [], json = csrfBody) => ({
11
+ status,
12
+ json: jest.fn().mockResolvedValue(json),
13
+ text: jest.fn().mockResolvedValue('error text'),
14
+ headers: { getSetCookie: () => setCookie, get: () => null },
15
+ });
16
+ const createConfig = (overrides) => ({
17
+ apiHost: 'https://flexplm.example.com',
18
+ urlContext: '/FlexPLM',
19
+ vibeEventEndpoint: '/api/events',
20
+ csrfEndpoint: '/csrf',
21
+ plmEnviornment: 'test',
22
+ userName: () => 'user',
23
+ password: () => 'super-secret-password',
24
+ itemPreDevelopmentLifecycleStages: [],
25
+ payloadDefaultAsArray: true,
26
+ useDistinctRestEndPointForImages: false,
27
+ ...overrides,
28
+ });
29
+ // The session cache is module level, so each test uses its own user to start cold.
30
+ let userCounter = 0;
31
+ const sessionConfig = () => {
32
+ const user = 'telemetry-user-' + (++userCounter);
33
+ return createConfig({ userName: () => user, flexplmConnect: { sessionReuse: { enabled: true } } });
34
+ };
35
+ const fetchMock = () => global.fetch;
36
+ const payload = { eventType: 'PERSIST', objectClass: 'LCSProduct', data: {} };
37
+ const allRecordedValues = (spans) => spans.flatMap(span => [
38
+ ...Object.values(span.attributes).map(String),
39
+ ...span.events.flatMap(event => [event.name, ...Object.values(event.attributes || {}).map(String)]),
40
+ ]);
41
+ describe('FlexPLMConnect telemetry', () => {
42
+ let exporter;
43
+ const globalFetch = global.fetch;
44
+ beforeAll(() => {
45
+ exporter = (0, telemetry_test_utils_1.registerInMemoryTracing)();
46
+ });
47
+ beforeEach(() => {
48
+ exporter.reset();
49
+ global.fetch = jest.fn();
50
+ });
51
+ afterEach(() => {
52
+ jest.restoreAllMocks();
53
+ });
54
+ afterAll(() => {
55
+ global.fetch = globalFetch;
56
+ });
57
+ it('should record a send_to_flexplm span with http.response.status_code 200', async () => {
58
+ fetchMock()
59
+ .mockResolvedValueOnce(resp(200))
60
+ .mockResolvedValueOnce(resp(200, [], { ok: true }));
61
+ await new flexplm_connect_1.FlexPLMConnect(createConfig()).sendToFlexPLM(payload);
62
+ const send = (0, telemetry_test_utils_1.findSpan)(exporter, 'send_to_flexplm');
63
+ expect(send.attributes).toEqual(expect.objectContaining({
64
+ [telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_KIND]: 'send',
65
+ [telemetry_attributes_1.ATTR_HTTP_REQUEST_METHOD]: 'POST',
66
+ [telemetry_attributes_1.ATTR_URL_PATH]: '/FlexPLM/servlet/rest/api/events',
67
+ [telemetry_attributes_1.ATTR_FLEXPLM_PAYLOAD_COUNT]: 1,
68
+ [telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED]: false,
69
+ [telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]: 200,
70
+ }));
71
+ expect(send.status.code).not.toBe(api_1.SpanStatusCode.ERROR);
72
+ const csrf = (0, telemetry_test_utils_1.findSpan)(exporter, 'fetch_flexplm_csrf');
73
+ expect(csrf.attributes).toEqual(expect.objectContaining({
74
+ [telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_KIND]: 'csrf',
75
+ [telemetry_attributes_1.ATTR_HTTP_REQUEST_METHOD]: 'GET',
76
+ [telemetry_attributes_1.ATTR_URL_PATH]: '/FlexPLM/csrf',
77
+ [telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]: 200,
78
+ }));
79
+ expect((0, telemetry_test_utils_1.isChildOf)(csrf, send)).toBe(true);
80
+ });
81
+ it('should set retried true when the cached session is rejected', async () => {
82
+ fetchMock()
83
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=first; Path=/']))
84
+ .mockResolvedValueOnce(resp(403))
85
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=second; Path=/']))
86
+ .mockResolvedValueOnce(resp(200, [], { ok: true }));
87
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig()).sendToFlexPLM(payload);
88
+ const send = (0, telemetry_test_utils_1.findSpan)(exporter, 'send_to_flexplm');
89
+ expect(send.attributes[telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED]).toBe(true);
90
+ expect(send.attributes[telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]).toBe(200);
91
+ expect(send.status.code).not.toBe(api_1.SpanStatusCode.ERROR);
92
+ });
93
+ it('should record a rejected cached session on the csrf call as an event, not an error', async () => {
94
+ const config = sessionConfig();
95
+ // Warm the cache with a cookie.
96
+ fetchMock()
97
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=cached; Path=/']))
98
+ .mockResolvedValueOnce(resp(200, [], { ok: true }));
99
+ await new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload);
100
+ exporter.reset();
101
+ fetchMock()
102
+ .mockResolvedValueOnce(resp(401))
103
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=fresh; Path=/']))
104
+ .mockResolvedValueOnce(resp(200, [], { ok: true }));
105
+ await new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload);
106
+ const [rejected, fresh] = (0, telemetry_test_utils_1.findSpans)(exporter, 'fetch_flexplm_csrf');
107
+ expect(rejected.events.map(event => event.name)).toEqual(['flexplm_session_rejected']);
108
+ expect(rejected.status.code).not.toBe(api_1.SpanStatusCode.ERROR);
109
+ expect(rejected.attributes[telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]).toBe(401);
110
+ expect(fresh.events).toEqual([]);
111
+ expect(fresh.attributes[telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]).toBe(200);
112
+ expect((0, telemetry_test_utils_1.findSpan)(exporter, 'send_to_flexplm').status.code).not.toBe(api_1.SpanStatusCode.ERROR);
113
+ });
114
+ it('should set the span status to ERROR when FlexPLM returns 500', async () => {
115
+ const errorLog = jest.spyOn(telemetry_1.logger, 'error').mockImplementation(() => undefined);
116
+ fetchMock()
117
+ .mockResolvedValueOnce(resp(200))
118
+ .mockResolvedValueOnce(resp(500));
119
+ await expect(new flexplm_connect_1.FlexPLMConnect(createConfig()).sendToFlexPLM(payload))
120
+ .rejects.toThrow('Error sending data to FlexPLM:status: 500');
121
+ const send = (0, telemetry_test_utils_1.findSpan)(exporter, 'send_to_flexplm');
122
+ expect(send.status.code).toBe(api_1.SpanStatusCode.ERROR);
123
+ expect(send.attributes[telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]).toBe(500);
124
+ expect(send.events.map(event => event.name)).toContain('exception');
125
+ expect(errorLog).toHaveBeenCalledWith({ flexplm: { status: 500, url_path: '/FlexPLM/servlet/rest/api/events', response_body: 'error text' } }, 'Error sending data to FlexPLM:status: 500');
126
+ });
127
+ it('should truncate the logged response body', async () => {
128
+ const errorLog = jest.spyOn(telemetry_1.logger, 'error').mockImplementation(() => undefined);
129
+ const failed = resp(500);
130
+ failed.text.mockResolvedValue('x'.repeat(5000));
131
+ fetchMock()
132
+ .mockResolvedValueOnce(resp(200))
133
+ .mockResolvedValueOnce(failed);
134
+ await expect(new flexplm_connect_1.FlexPLMConnect(createConfig()).sendToFlexPLM(payload)).rejects.toThrow();
135
+ const logged = errorLog.mock.calls[0][0].flexplm.response_body;
136
+ expect(logged.length).toBeLessThan(2100);
137
+ expect(logged.endsWith('...(truncated)')).toBe(true);
138
+ });
139
+ it('should record only the path of a getRequest, without the host or query string', async () => {
140
+ fetchMock().mockResolvedValueOnce(resp(200, [], { ok: true }));
141
+ await new flexplm_connect_1.FlexPLMConnect(createConfig()).getRequest({ urlPath: '/servlet/rest/rfa/vibeiq/image?path=%2Fsecret%2Ffile.png' });
142
+ const get = (0, telemetry_test_utils_1.findSpan)(exporter, 'get_from_flexplm');
143
+ expect(get.attributes).toEqual(expect.objectContaining({
144
+ [telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_KIND]: 'get',
145
+ [telemetry_attributes_1.ATTR_URL_PATH]: '/FlexPLM/servlet/rest/rfa/vibeiq/image',
146
+ [telemetry_attributes_1.ATTR_HTTP_RESPONSE_STATUS_CODE]: 200,
147
+ [telemetry_attributes_1.ATTR_FLEXPLM_REQUEST_RETRIED]: false,
148
+ }));
149
+ });
150
+ it('should record the session source: disabled, then new, then cached', async () => {
151
+ fetchMock().mockResolvedValueOnce(resp(200)).mockResolvedValueOnce(resp(200, [], { ok: true }));
152
+ await new flexplm_connect_1.FlexPLMConnect(createConfig()).sendToFlexPLM(payload);
153
+ expect((0, telemetry_test_utils_1.findSpan)(exporter, 'send_to_flexplm').attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]).toBe('disabled');
154
+ const config = sessionConfig();
155
+ for (let i = 0; i < 2; i++) {
156
+ fetchMock().mockResolvedValueOnce(resp(200, ['JSESSIONID=reused; Path=/'])).mockResolvedValueOnce(resp(200, [], { ok: true }));
157
+ }
158
+ exporter.reset();
159
+ await new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload);
160
+ await new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload);
161
+ const [first, second] = (0, telemetry_test_utils_1.findSpans)(exporter, 'send_to_flexplm');
162
+ expect(first.attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]).toBe('new');
163
+ expect(second.attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]).toBe('cached');
164
+ expect(first.attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_WAITED]).toBeUndefined();
165
+ });
166
+ it('should record session.source new after a rejected cached session is replaced', async () => {
167
+ const config = sessionConfig();
168
+ fetchMock().mockResolvedValueOnce(resp(200, ['JSESSIONID=cached; Path=/'])).mockResolvedValueOnce(resp(200, [], { ok: true }));
169
+ await new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload);
170
+ exporter.reset();
171
+ fetchMock()
172
+ .mockResolvedValueOnce(resp(401))
173
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=fresh; Path=/']))
174
+ .mockResolvedValueOnce(resp(200, [], { ok: true }));
175
+ await new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload);
176
+ expect((0, telemetry_test_utils_1.findSpan)(exporter, 'send_to_flexplm').attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]).toBe('new');
177
+ });
178
+ it('should record session.waited when a request waits for another request\'s session acquisition', async () => {
179
+ const config = sessionConfig();
180
+ let release;
181
+ const gate = new Promise(r => { release = r; });
182
+ fetchMock().mockImplementation(async (_url, opts) => {
183
+ if (opts.method === 'GET' && !opts.headers.Cookie) {
184
+ await gate;
185
+ return resp(200, ['JSESSIONID=shared; Path=/']);
186
+ }
187
+ return opts.method === 'GET' ? resp(200) : resp(200, [], { ok: true });
188
+ });
189
+ const all = Promise.all([1, 2].map(() => new flexplm_connect_1.FlexPLMConnect(config).sendToFlexPLM(payload)));
190
+ await new Promise(r => setImmediate(r));
191
+ release();
192
+ await all;
193
+ const spans = (0, telemetry_test_utils_1.findSpans)(exporter, 'send_to_flexplm');
194
+ expect(spans.map(span => span.attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_WAITED]).sort()).toEqual([true, undefined]);
195
+ expect(spans.map(span => span.attributes[telemetry_attributes_1.ATTR_FLEXPLM_SESSION_SOURCE]).sort()).toEqual(['cached', 'new']);
196
+ });
197
+ it('should not record Authorization, Cookie or nonce values on any span', async () => {
198
+ fetchMock()
199
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=cookie-value-1; Path=/']))
200
+ .mockResolvedValueOnce(resp(403))
201
+ .mockResolvedValueOnce(resp(200, ['JSESSIONID=cookie-value-2; Path=/']))
202
+ .mockResolvedValueOnce(resp(200, [], { ok: true }));
203
+ await new flexplm_connect_1.FlexPLMConnect(sessionConfig()).sendToFlexPLM(payload);
204
+ const spans = exporter.getFinishedSpans();
205
+ expect(spans.length).toBeGreaterThan(0);
206
+ const recorded = allRecordedValues(spans).join('\n');
207
+ const authorization = Buffer.from('telemetry-user-' + userCounter + ':super-secret-password').toString('base64');
208
+ for (const secret of ['super-secret-password', authorization, 'cookie-value-1', 'cookie-value-2', 'secret-nonce-value', 'CSRF_NONCE', 'flexplm.example.com']) {
209
+ expect(recorded).not.toContain(secret);
210
+ }
211
+ });
212
+ });
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setLoggerConfig = void 0;
3
+ exports.setLoggerConfig = setLoggerConfig;
4
4
  const app_framework_1 = require("@contrail/app-framework");
5
5
  async function setLoggerConfig(appConfig) {
6
6
  let logLevel = app_framework_1.LogLevel.INFO;
@@ -24,4 +24,3 @@ async function setLoggerConfig(appConfig) {
24
24
  }
25
25
  app_framework_1.Logger.setConfig({ logLevel: logLevel });
26
26
  }
27
- exports.setLoggerConfig = setLoggerConfig;