@contrail/flexplm 1.8.1 → 2.0.0-alpha.c9d1df5
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/lib/__tests__/telemetry-test-utils.d.ts +8 -0
- package/lib/__tests__/telemetry-test-utils.js +30 -0
- package/lib/cli/commands/compile.js +17 -7
- package/lib/cli/commands/compile.spec.js +17 -7
- package/lib/cli/commands/create.js +17 -7
- package/lib/cli/commands/create.spec.js +17 -7
- package/lib/cli/commands/upload.js +17 -7
- package/lib/cli/index.js +2 -2
- package/lib/entity-processor/base-entity-processor.d.ts +29 -0
- package/lib/entity-processor/base-entity-processor.js +238 -143
- package/lib/entity-processor/base-entity-processor.spec.js +210 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/publish/base-process-publish-assortment-callback.js +30 -0
- package/lib/publish/base-process-publish-assortment-callback.spec.d.ts +1 -0
- package/lib/publish/base-process-publish-assortment-callback.spec.js +62 -0
- package/lib/publish/base-process-publish-assortment.d.ts +24 -0
- package/lib/publish/base-process-publish-assortment.js +238 -132
- package/lib/publish/base-process-publish-assortment.spec.js +188 -0
- package/lib/util/error-response-object.js +2 -0
- package/lib/util/event-short-message-status.js +1 -1
- package/lib/util/flexplm-connect.d.ts +4 -0
- package/lib/util/flexplm-connect.js +210 -145
- package/lib/util/flexplm-connect.telemetry.spec.d.ts +1 -0
- package/lib/util/flexplm-connect.telemetry.spec.js +212 -0
- package/lib/util/logger-config.js +1 -2
- package/lib/util/telemetry-attributes.d.ts +68 -0
- package/lib/util/telemetry-attributes.js +73 -0
- package/lib/util/telemetry-utils.d.ts +3 -0
- package/lib/util/telemetry-utils.js +10 -0
- package/lib/util/thumbnail-util.d.ts +2 -1
- package/lib/util/thumbnail-util.js +35 -7
- package/lib/util/thumbnail-util.spec.js +42 -0
- package/package.json +10 -3
|
@@ -10,6 +10,11 @@ const type_conversion_utils_1 = require("../util/type-conversion-utils");
|
|
|
10
10
|
const map_utils_1 = require("../util/map-utils");
|
|
11
11
|
const item_family_changes_1 = require("../interfaces/item-family-changes");
|
|
12
12
|
const flexplm_connect_1 = require("../util/flexplm-connect");
|
|
13
|
+
const telemetry_1 = require("@contrail/telemetry");
|
|
14
|
+
const api_1 = require("@opentelemetry/api");
|
|
15
|
+
const telemetry_test_utils_1 = require("../__tests__/telemetry-test-utils");
|
|
16
|
+
const event_short_message_status_1 = require("../util/event-short-message-status");
|
|
17
|
+
const telemetry_attributes_1 = require("../util/telemetry-attributes");
|
|
13
18
|
let federatedId = '';
|
|
14
19
|
jest.mock('../util/data-converter', () => {
|
|
15
20
|
return {
|
|
@@ -2211,3 +2216,186 @@ describe('released family with pre-development options', () => {
|
|
|
2211
2216
|
expect(await ppa.getEventsForItemFamilyChanges(ifc, 'a1', eventSeasonFed, new Map())).toHaveLength(0);
|
|
2212
2217
|
});
|
|
2213
2218
|
});
|
|
2219
|
+
describe('publish status telemetry', () => {
|
|
2220
|
+
const config = {};
|
|
2221
|
+
const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
|
|
2222
|
+
const pcd = { assortmentId: 'a1', assortmentPublishChangeId: 'apc1' };
|
|
2223
|
+
let exporter;
|
|
2224
|
+
let ppa;
|
|
2225
|
+
let infoLog;
|
|
2226
|
+
let warnLog;
|
|
2227
|
+
let errorLog;
|
|
2228
|
+
beforeAll(() => {
|
|
2229
|
+
exporter = (0, telemetry_test_utils_1.registerInMemoryTracing)();
|
|
2230
|
+
});
|
|
2231
|
+
beforeEach(() => {
|
|
2232
|
+
exporter.reset();
|
|
2233
|
+
federatedId = '';
|
|
2234
|
+
entityObject = { publishToFlexPLM: true };
|
|
2235
|
+
ppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, new data_converter_1.DataConverter(config, mapFileUtil), mapFileUtil);
|
|
2236
|
+
infoLog = jest.spyOn(telemetry_1.logger, 'info').mockImplementation(() => undefined);
|
|
2237
|
+
warnLog = jest.spyOn(telemetry_1.logger, 'warn').mockImplementation(() => undefined);
|
|
2238
|
+
errorLog = jest.spyOn(telemetry_1.logger, 'error').mockImplementation(() => undefined);
|
|
2239
|
+
});
|
|
2240
|
+
afterEach(() => {
|
|
2241
|
+
jest.restoreAllMocks();
|
|
2242
|
+
});
|
|
2243
|
+
const stubProcessPublish = (events) => {
|
|
2244
|
+
jest.spyOn(ppa, 'getFullChangeAssortmentMap').mockReturnValue(new Map());
|
|
2245
|
+
jest.spyOn(ppa, 'getDeleteChangesAssortmentMap').mockReturnValue(new Map());
|
|
2246
|
+
jest.spyOn(ppa, 'getItemFamilyChanges').mockReturnValue(new Map());
|
|
2247
|
+
jest.spyOn(ppa, 'getEventsForPublishChangeData').mockResolvedValue(events);
|
|
2248
|
+
jest.spyOn(ppa, 'sendEvents').mockResolvedValue({});
|
|
2249
|
+
};
|
|
2250
|
+
it('should record No_Federation_Information as a warning and keep the span OK', async () => {
|
|
2251
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierProperties').mockImplementation(async () => ['flexPLMSeasonName']);
|
|
2252
|
+
jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getInformationalProperties').mockImplementation(async () => ['name']);
|
|
2253
|
+
await ppa.process({ assortmentId: 'a1', assortmentPublishChangeId: 'apc1' });
|
|
2254
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'publish_assortment');
|
|
2255
|
+
expect(span.status.code).not.toBe(api_1.SpanStatusCode.ERROR);
|
|
2256
|
+
expect(span.attributes).toEqual(expect.objectContaining({
|
|
2257
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_STATUS]: event_short_message_status_1.EventShortMessageStatus.NO_FEDERATION_INFO,
|
|
2258
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_ASSORTMENT_ID]: 'a1',
|
|
2259
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_CHANGE_ID]: 'apc1',
|
|
2260
|
+
}));
|
|
2261
|
+
expect(warnLog).toHaveBeenCalledTimes(1);
|
|
2262
|
+
expect(warnLog).toHaveBeenCalledWith({ flexplm: expect.objectContaining({
|
|
2263
|
+
direction: 'publish', status: event_short_message_status_1.EventShortMessageStatus.NO_FEDERATION_INFO, assortment_id: 'a1', publish_change_id: 'apc1',
|
|
2264
|
+
reason: expect.stringContaining(base_process_publish_assortment_1.BaseProcessPublishAssortment.ASSORTMENT_NO_FED_INFO),
|
|
2265
|
+
}) }, 'FlexPLM assortment publish skipped');
|
|
2266
|
+
});
|
|
2267
|
+
it('should log No_Events_to_Send as an info log', async () => {
|
|
2268
|
+
stubProcessPublish([]);
|
|
2269
|
+
const result = await (0, telemetry_1.traceSection)('test', {}, () => ppa.processPublish(pcd, {}, {}, []));
|
|
2270
|
+
expect(result['skip_await']).toBe(true);
|
|
2271
|
+
expect(infoLog).toHaveBeenCalledWith({ flexplm: expect.objectContaining({
|
|
2272
|
+
status: event_short_message_status_1.EventShortMessageStatus.NO_EVENTS_TO_SEND, assortment_id: 'a1', publish_change_id: 'apc1',
|
|
2273
|
+
}) }, 'FlexPLM assortment publish processed');
|
|
2274
|
+
});
|
|
2275
|
+
it('should record the sent event counts on the span and in the info log', async () => {
|
|
2276
|
+
stubProcessPublish([{ objectClass: 'LCSProductSeasonLink' }]);
|
|
2277
|
+
await (0, telemetry_1.traceSection)('test', {}, () => ppa.processPublish(pcd, {}, {}, []));
|
|
2278
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'test');
|
|
2279
|
+
expect(span.attributes).toEqual(expect.objectContaining({
|
|
2280
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_STATUS]: event_short_message_status_1.EventShortMessageStatus.SENDING_EVENTS,
|
|
2281
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_PRODUCT_SEASON_COUNT]: 1,
|
|
2282
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_COLORWAY_SEASON_COUNT]: 0,
|
|
2283
|
+
}));
|
|
2284
|
+
expect(infoLog).toHaveBeenCalledWith({ flexplm: expect.objectContaining({
|
|
2285
|
+
status: event_short_message_status_1.EventShortMessageStatus.SENDING_EVENTS, product_season_count: 1, colorway_season_count: 0,
|
|
2286
|
+
}) }, 'FlexPLM assortment publish processed');
|
|
2287
|
+
});
|
|
2288
|
+
it('should mark a failed publish file download as an error span and return undefined', async () => {
|
|
2289
|
+
const globalFetch = global.fetch;
|
|
2290
|
+
global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 403, json: jest.fn() });
|
|
2291
|
+
try {
|
|
2292
|
+
const result = await ppa.downloadHydratedChangeDetail({ hydratedDetailDownloadLink: 'https://signed.example/detail?sig=secret' });
|
|
2293
|
+
expect(result).toBeUndefined();
|
|
2294
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'download_publish_file');
|
|
2295
|
+
expect(span.status.code).toBe(api_1.SpanStatusCode.ERROR);
|
|
2296
|
+
expect(span.attributes).toEqual(expect.objectContaining({ [telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_FILE]: 'hydrated_detail', 'http.response.status_code': 403 }));
|
|
2297
|
+
expect(JSON.stringify(span.attributes)).not.toContain('secret');
|
|
2298
|
+
expect(errorLog).toHaveBeenCalledWith({
|
|
2299
|
+
error: expect.any(Error), flexplm: { direction: 'publish', publish_file: 'hydrated_detail', http_status: 403 },
|
|
2300
|
+
}, 'FlexPLM publish file download failed');
|
|
2301
|
+
}
|
|
2302
|
+
finally {
|
|
2303
|
+
global.fetch = globalFetch;
|
|
2304
|
+
}
|
|
2305
|
+
});
|
|
2306
|
+
it('should record failed downloads on the publish span instead of zero counts', async () => {
|
|
2307
|
+
jest.spyOn(ppa, 'getSeasonFederation').mockResolvedValue({});
|
|
2308
|
+
jest.spyOn(ppa, 'getApcHistory').mockResolvedValue([]);
|
|
2309
|
+
jest.spyOn(ppa, 'getSinceDate').mockResolvedValue(new Date());
|
|
2310
|
+
jest.spyOn(ppa, 'downloadAssortmentPublishChange').mockResolvedValue({ createdBy: {} });
|
|
2311
|
+
jest.spyOn(ppa, 'downloadHydratedChangeDetail').mockResolvedValue(undefined);
|
|
2312
|
+
jest.spyOn(ppa, 'downloadAssortmentBaseline').mockResolvedValue({});
|
|
2313
|
+
jest.spyOn(ppa, 'getDeleteChanges').mockResolvedValue([]);
|
|
2314
|
+
jest.spyOn(ppa, 'getReleasedForDevelopmentItemAndFamilyIds').mockReturnValue([]);
|
|
2315
|
+
jest.spyOn(ppa, 'processPublish').mockResolvedValue({});
|
|
2316
|
+
jest.spyOn(ppa, 'getPublishInfo').mockResolvedValue({});
|
|
2317
|
+
await ppa.process({ assortmentId: 'a1', assortmentPublishChangeId: 'apc1' });
|
|
2318
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'publish_assortment');
|
|
2319
|
+
expect(span.attributes[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_FAILED_DOWNLOADS]).toEqual(['hydrated_detail']);
|
|
2320
|
+
expect(span.attributes[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_ADDED_COUNT]).toBeUndefined();
|
|
2321
|
+
});
|
|
2322
|
+
it('should record the send mode and warn when it is unknown', async () => {
|
|
2323
|
+
ppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment({ sendMode: {} }, new data_converter_1.DataConverter(config, mapFileUtil), mapFileUtil);
|
|
2324
|
+
const result = await (0, telemetry_1.traceSection)('test', {}, () => ppa.sendEvents([{}, {}]));
|
|
2325
|
+
expect(result).toEqual({});
|
|
2326
|
+
expect((0, telemetry_test_utils_1.findSpan)(exporter, 'test').attributes[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_SEND_MODE]).toBe('');
|
|
2327
|
+
expect(warnLog).toHaveBeenCalledWith({ flexplm: { direction: 'publish', send_mode: '', payload_count: 2 } }, 'Unknown FlexPLM publish sendMode; events not sent');
|
|
2328
|
+
});
|
|
2329
|
+
it('should log a publish FlexPLM accepted but that was not recorded as sent_unrecorded', async () => {
|
|
2330
|
+
const failure = new Error('external-event create failed');
|
|
2331
|
+
failure.sendToFlexPLMResult = { status: 200 };
|
|
2332
|
+
failure.sendPublishPayloadEventResult = { error: failure.message };
|
|
2333
|
+
jest.spyOn(ppa, 'getSeasonFederation').mockResolvedValue({});
|
|
2334
|
+
jest.spyOn(ppa, 'getApcHistory').mockRejectedValue(failure);
|
|
2335
|
+
jest.spyOn(ppa, 'getPublishInfo').mockResolvedValue({});
|
|
2336
|
+
await expect(ppa.process({ assortmentId: 'a1', assortmentPublishChangeId: 'apc1' })).rejects.toBe(failure);
|
|
2337
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'publish_assortment');
|
|
2338
|
+
expect(span.attributes[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_SENT_UNRECORDED]).toBe(true);
|
|
2339
|
+
expect(errorLog).toHaveBeenCalledWith({
|
|
2340
|
+
error: failure, flexplm: expect.objectContaining({ status: event_short_message_status_1.EventShortMessageStatus.FAILURE, sent_unrecorded: true }),
|
|
2341
|
+
}, 'FlexPLM assortment publish sent but not recorded');
|
|
2342
|
+
});
|
|
2343
|
+
it('should record the history count on get_publish_history', async () => {
|
|
2344
|
+
sdk_1.Request.request = jest.fn().mockResolvedValue([{ id: 'apc1' }, { id: 'apc2' }]);
|
|
2345
|
+
try {
|
|
2346
|
+
await ppa.getApcHistory('a1');
|
|
2347
|
+
}
|
|
2348
|
+
finally {
|
|
2349
|
+
delete sdk_1.Request.request;
|
|
2350
|
+
}
|
|
2351
|
+
expect((0, telemetry_test_utils_1.findSpan)(exporter, 'get_publish_history').attributes[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_HISTORY_COUNT]).toBe(2);
|
|
2352
|
+
});
|
|
2353
|
+
it('should record the delete change count on get_delete_changes', async () => {
|
|
2354
|
+
jest.spyOn(ppa, 'downloadDeleteChanges').mockResolvedValue([{ itemId: 'i1' }, { itemId: 'i2' }]);
|
|
2355
|
+
const apcHistory = [{ id: 'apc0', createdOn: '2026-01-01' }, { id: 'apc1', createdOn: '2026-02-01' }];
|
|
2356
|
+
const deletes = await ppa.getDeleteChanges({ id: 'apc1' }, apcHistory, { assortmentItems: [] }, new Date('2026-01-01'));
|
|
2357
|
+
expect(deletes).toHaveLength(2);
|
|
2358
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'get_delete_changes');
|
|
2359
|
+
expect(span.attributes[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_DELETE_CHANGE_COUNT]).toBe(2);
|
|
2360
|
+
});
|
|
2361
|
+
it('should record item family and payload counts on build_publish_events', async () => {
|
|
2362
|
+
const changes = new Map([['fam1', {}], ['fam2', {}]]);
|
|
2363
|
+
jest.spyOn(ppa, 'getEventsForItemFamilyChanges')
|
|
2364
|
+
.mockResolvedValueOnce([{}, {}]).mockResolvedValueOnce([{}]);
|
|
2365
|
+
const events = await ppa.getEventsForPublishChangeData({ ...pcd, itemFamilyChanges: changes });
|
|
2366
|
+
expect(events).toHaveLength(3);
|
|
2367
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'build_publish_events');
|
|
2368
|
+
expect(span.attributes).toEqual(expect.objectContaining({
|
|
2369
|
+
[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_ITEM_FAMILY_COUNT]: 2, [telemetry_attributes_1.ATTR_FLEXPLM_PAYLOAD_COUNT]: 3,
|
|
2370
|
+
}));
|
|
2371
|
+
});
|
|
2372
|
+
it('should count skipped deletes with no item entity and log one warning for the publish', async () => {
|
|
2373
|
+
const releasedPcd = {
|
|
2374
|
+
assortmentId: 'a1', assortmentPublishChangeId: 'apc1', sinceDate: new Date(), itemToFederatedIdMapping: new Map(),
|
|
2375
|
+
// A deleted entry without its item has no family id; it is only in scope if that undefined id is.
|
|
2376
|
+
releasedForDevelopmentItemIds: [undefined, 'opt1'],
|
|
2377
|
+
};
|
|
2378
|
+
const emptyDetail = { adds: [], deletes: [], updates: [], familyItemsRemoved: [] };
|
|
2379
|
+
await (0, telemetry_1.traceSection)('test', {}, async () => ppa.getItemFamilyChanges(releasedPcd, emptyDetail, new Map(), new Map([['opt1', { itemId: 'opt1' }]])));
|
|
2380
|
+
expect((0, telemetry_test_utils_1.findSpan)(exporter, 'test').attributes[telemetry_attributes_1.ATTR_FLEXPLM_PUBLISH_SKIPPED_DELETE_COUNT]).toBe(1);
|
|
2381
|
+
expect(warnLog).toHaveBeenCalledTimes(1);
|
|
2382
|
+
expect(warnLog).toHaveBeenCalledWith({ flexplm: expect.objectContaining({
|
|
2383
|
+
assortment_id: 'a1', skipped_delete_count: 1, skipped_deletes: [{ item_family_id: undefined, item_id: 'opt1' }],
|
|
2384
|
+
}) }, 'FlexPLM publish skipped deleted items with no item entity');
|
|
2385
|
+
});
|
|
2386
|
+
it('should log a failed publish with the error and rethrow it', async () => {
|
|
2387
|
+
const failure = new Error('apc history unavailable');
|
|
2388
|
+
jest.spyOn(ppa, 'getSeasonFederation').mockResolvedValue({});
|
|
2389
|
+
jest.spyOn(ppa, 'getApcHistory').mockRejectedValue(failure);
|
|
2390
|
+
jest.spyOn(ppa, 'getPublishInfo').mockResolvedValue({});
|
|
2391
|
+
await expect(ppa.process({ assortmentId: 'a1', assortmentPublishChangeId: 'apc1' })).rejects.toBe(failure);
|
|
2392
|
+
const span = (0, telemetry_test_utils_1.findSpan)(exporter, 'publish_assortment');
|
|
2393
|
+
expect(span.status.code).toBe(api_1.SpanStatusCode.ERROR);
|
|
2394
|
+
expect(span.attributes[telemetry_attributes_1.ATTR_FLEXPLM_STATUS]).toBe(event_short_message_status_1.EventShortMessageStatus.FAILURE);
|
|
2395
|
+
expect(errorLog).toHaveBeenCalledWith({
|
|
2396
|
+
error: failure,
|
|
2397
|
+
flexplm: expect.objectContaining({ status: event_short_message_status_1.EventShortMessageStatus.FAILURE, assortment_id: 'a1', publish_change_id: 'apc1' }),
|
|
2398
|
+
}, 'FlexPLM assortment publish failed');
|
|
2399
|
+
expect(infoLog).not.toHaveBeenCalled();
|
|
2400
|
+
});
|
|
2401
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ErrorResponseObject = void 0;
|
|
4
4
|
const app_framework_1 = require("@contrail/app-framework");
|
|
5
|
+
const telemetry_1 = require("@contrail/telemetry");
|
|
5
6
|
class ErrorResponseObject {
|
|
6
7
|
/** Returns a response, checking for multiple possible error details
|
|
7
8
|
*
|
|
@@ -48,6 +49,7 @@ class ErrorResponseObject {
|
|
|
48
49
|
if (e.stack) {
|
|
49
50
|
console.log('ErrorResponseObject.getResponse stack: ' + e.stack);
|
|
50
51
|
}
|
|
52
|
+
telemetry_1.logger.error({ error: e }, 'FlexPLM action failed');
|
|
51
53
|
return response;
|
|
52
54
|
}
|
|
53
55
|
}
|
|
@@ -21,4 +21,4 @@ var EventShortMessageStatus;
|
|
|
21
21
|
EventShortMessageStatus["NO_FEDERATION_INFO"] = "No_Federation_Information";
|
|
22
22
|
EventShortMessageStatus["NO_EVENTS_TO_SEND"] = "No_Events_to_Send";
|
|
23
23
|
EventShortMessageStatus["SENDING_EVENTS"] = "Sending_Events";
|
|
24
|
-
})(EventShortMessageStatus
|
|
24
|
+
})(EventShortMessageStatus || (exports.EventShortMessageStatus = EventShortMessageStatus = {}));
|
|
@@ -12,6 +12,10 @@ export declare class FlexPLMConnect {
|
|
|
12
12
|
* and the check-then-register in runColdAcquisition has to stay synchronous to be race free.
|
|
13
13
|
*/
|
|
14
14
|
private waitForInFlightAcquisition;
|
|
15
|
+
/** Marks the active span when this request is about to wait for another request's cold acquisition.
|
|
16
|
+
* Synchronous on purpose: it must not add an await to the wait loops (see waitForInFlightAcquisition).
|
|
17
|
+
*/
|
|
18
|
+
private recordWaitForInFlightAcquisition;
|
|
15
19
|
/** Runs a cold acquisition and registers it so concurrent callers wait for it instead of opening their own session. */
|
|
16
20
|
private runColdAcquisition;
|
|
17
21
|
private getSessionKey;
|