From b6e616e19b82fe6d0562b743db31306948e39ad1 Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Wed, 23 Sep 2026 16:47:00 +0100 Subject: [PATCH 1/3] fix(npmjs): follower crashes on truncated registry responses Since 13:00 UTC the registry intermittently truncates gzip response bodies. Zlib raises Z_BUF_ERROR for these, which was not treated as retryable, so a single truncated packument crashed the whole run before the state was saved. On Gamma no state had been persisted for two hours, with 20-36 failed invocations per hour. Three changes: - Z_BUF_ERROR (truncated body) is retried like ECONNRESET. - A packument fetch that still fails no longer fails the batch. The entry is excluded from the receipts instead, so a later scan retries it. - The follower state is saved even when a scan fails part-way, so completed work (receipts, interim checkpoints, staged versions) is never lost to a crash. --- .../__snapshots__/construct-hub.test.ts.snap | 10 +- .../__snapshots__/snapshot.test.ts.snap | 2 +- .../npmjs/npm-js-follower.lambda.test.ts | 107 +++++++++++++++++- .../npmjs/couch-changes.lambda-shared.ts | 65 +++++++++-- .../npmjs/npm-js-follower.lambda.ts | 100 ++++++++-------- 5 files changed, 222 insertions(+), 62 deletions(-) diff --git a/src/__tests__/__snapshots__/construct-hub.test.ts.snap b/src/__tests__/__snapshots__/construct-hub.test.ts.snap index 3586de3a4..d1115220e 100644 --- a/src/__tests__/__snapshots__/construct-hub.test.ts.snap +++ b/src/__tests__/__snapshots__/construct-hub.test.ts.snap @@ -9974,7 +9974,7 @@ RunBook: https://github.com/cdklabs/construct-hub/blob/main/docs/operator-runboo "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "5890a5f32d4ca5288bf1b5299a0b6cb482820a5711468d81598bf681d7d34909.zip", + "S3Key": "1387dd527fc81c8e9e360d21d3c6c29f9121c41ff623ea54b4f28ccedcfc567a.zip", }, "Description": "[Test/ConstructHub/Sources/NpmJs] Periodically query npmjs.com index for new packages", "Environment": { @@ -24427,7 +24427,7 @@ RunBook: https://github.com/cdklabs/construct-hub/blob/main/docs/operator-runboo "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "5890a5f32d4ca5288bf1b5299a0b6cb482820a5711468d81598bf681d7d34909.zip", + "S3Key": "1387dd527fc81c8e9e360d21d3c6c29f9121c41ff623ea54b4f28ccedcfc567a.zip", }, "Description": "[Test/ConstructHub/Sources/NpmJs] Periodically query npmjs.com index for new packages", "Environment": { @@ -38444,7 +38444,7 @@ RunBook: https://github.com/cdklabs/construct-hub/blob/main/docs/operator-runboo "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "5890a5f32d4ca5288bf1b5299a0b6cb482820a5711468d81598bf681d7d34909.zip", + "S3Key": "1387dd527fc81c8e9e360d21d3c6c29f9121c41ff623ea54b4f28ccedcfc567a.zip", }, "Description": "[Test/ConstructHub/Sources/NpmJs] Periodically query npmjs.com index for new packages", "Environment": { @@ -52618,7 +52618,7 @@ RunBook: https://github.com/cdklabs/construct-hub/blob/main/docs/operator-runboo "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "5890a5f32d4ca5288bf1b5299a0b6cb482820a5711468d81598bf681d7d34909.zip", + "S3Key": "1387dd527fc81c8e9e360d21d3c6c29f9121c41ff623ea54b4f28ccedcfc567a.zip", }, "Description": "[Test/ConstructHub/Sources/NpmJs] Periodically query npmjs.com index for new packages", "Environment": { @@ -66775,7 +66775,7 @@ RunBook: https://github.com/cdklabs/construct-hub/blob/main/docs/operator-runboo "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "5890a5f32d4ca5288bf1b5299a0b6cb482820a5711468d81598bf681d7d34909.zip", + "S3Key": "1387dd527fc81c8e9e360d21d3c6c29f9121c41ff623ea54b4f28ccedcfc567a.zip", }, "Description": "[Test/ConstructHub/Sources/NpmJs] Periodically query npmjs.com index for new packages", "Environment": { diff --git a/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap b/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap index 69aa58539..7ec886c78 100644 --- a/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap +++ b/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap @@ -11249,7 +11249,7 @@ Direct link to the function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "5890a5f32d4ca5288bf1b5299a0b6cb482820a5711468d81598bf681d7d34909.zip", + "S3Key": "1387dd527fc81c8e9e360d21d3c6c29f9121c41ff623ea54b4f28ccedcfc567a.zip", }, "Description": "[dev/ConstructHub/Sources/NpmJs] Periodically query npmjs.com index for new packages", "Environment": { diff --git a/src/__tests__/package-sources/npmjs/npm-js-follower.lambda.test.ts b/src/__tests__/package-sources/npmjs/npm-js-follower.lambda.test.ts index 1bcc876c9..bdeb2e97b 100644 --- a/src/__tests__/package-sources/npmjs/npm-js-follower.lambda.test.ts +++ b/src/__tests__/package-sources/npmjs/npm-js-follower.lambda.test.ts @@ -1,4 +1,4 @@ -import { gunzipSync } from 'zlib'; +import { gunzipSync, gzipSync } from 'zlib'; import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'; import { GetObjectCommand, @@ -121,6 +121,7 @@ afterEach(() => { delete process.env[ENV_DENY_LIST_OBJECT_KEY]; delete process.env[LicenseListEnv.BUCKET_NAME]; delete process.env[LicenseListEnv.OBJECT_KEY]; + delete process.env.REQUEST_DEADLINE_MS; nock.cleanAll(); }); @@ -522,6 +523,110 @@ test('records interim checkpoints so an interrupted scan resumes mid-window', as expect(saved.lastDeepScanAt).toBeUndefined(); }); +test('retries truncated gzip responses from the registry', async () => { + givenHead(1_010); + givenNoStateFile(); + givenLegacyMarker(1_000); + + nock(REPLICA) + .get('/registry/_changes') + .query({ limit: '10000', since: '1000' }) + .reply(200, { + results: [ + { + seq: 1_005, + id: 'flaky-package', + changes: [{ rev: '1-aaaaaa' }], + deleted: false, + }, + ], + last_seq: 1_005, + }); + // First packument response is a truncated gzip body (Z_BUF_ERROR), the + // retry succeeds. + const fullBody = gzipSync( + Buffer.from( + JSON.stringify(packageDoc('flaky-package', '1-aaaaaa', '1.0.0')) + ) + ); + nock(REGISTRY) + .get('/flaky-package') + .reply(200, fullBody.subarray(0, fullBody.length - 8), { + 'content-encoding': 'gzip', + }); + nock(REGISTRY) + .get('/flaky-package') + .reply(200, packageDoc('flaky-package', '1-aaaaaa', '1.0.0')); + + await handler(event, context); + + expect(stagedPackages()).toEqual([ + { name: 'flaky-package', version: '1.0.0' }, + ]); + expect(savedState().has(1_005)).toBe(true); +}); + +test('does not receipt entries whose metadata fetch failed, so a later scan retries them', async () => { + process.env.REQUEST_DEADLINE_MS = '0'; // do not wait out transient retries + givenHead(1_010); + givenNoStateFile(); + givenLegacyMarker(1_000); + + nock(REPLICA) + .get('/registry/_changes') + .query({ limit: '10000', since: '1000' }) + .reply(200, { + results: [ + { + seq: 1_004, + id: 'broken-package', + changes: [{ rev: '1-aaaaaa' }], + deleted: false, + }, + { + seq: 1_006, + id: 'good-package', + changes: [{ rev: '1-bbbbbb' }], + deleted: false, + }, + ], + last_seq: 1_006, + }); + nock(REGISTRY).get('/broken-package').reply(500, 'oops'); + nock(REGISTRY) + .get('/good-package') + .reply(200, packageDoc('good-package', '1-bbbbbb', '1.0.0')); + + await handler(event, context); + + // The healthy entry was processed and receipted; the failed one was not + // receipted, so the next scan will retry it. + expect(stagedPackages()).toEqual([ + { name: 'good-package', version: '1.0.0' }, + ]); + const state = savedState(); + expect(state.has(1_006)).toBe(true); + expect(state.has(1_004)).toBe(false); +}); + +test('persists its state even when the scan fails part-way', async () => { + process.env.REQUEST_DEADLINE_MS = '0'; + givenHead(1_010); + givenNoStateFile(); + givenLegacyMarker(1_000); + + // The feed itself is unavailable: the scan throws. + nock(REPLICA) + .get('/registry/_changes') + .query({ limit: '10000', since: '1000' }) + .reply(503, 'unavailable'); + + await expect(handler(event, context)).rejects.toThrow(/HTTP 503/); + + // The state (here: the seeded checkpoint) was saved regardless. + expect(savedState().newestCheckpointSeq()).toBe(1_000); +}); + test('resets its position when the feed head regresses below the newest checkpoint', async () => { const now = Date.now(); // The feed head (3000) is below our newest checkpoint (5000). diff --git a/src/package-sources/npmjs/couch-changes.lambda-shared.ts b/src/package-sources/npmjs/couch-changes.lambda-shared.ts index dc15e114a..d28ae2128 100644 --- a/src/package-sources/npmjs/couch-changes.lambda-shared.ts +++ b/src/package-sources/npmjs/couch-changes.lambda-shared.ts @@ -8,7 +8,14 @@ import { createGunzip } from 'zlib'; const NPM_REGISTRY_URL = 'https://registry.npmjs.org/'; -const REQUEST_DEADLINE_MS = 30_000; +/** + * How long to keep retrying transient request failures. Can be overridden + * through the environment for testing purposes. + */ +function requestDeadlineMs(): number { + const fromEnv = process.env.REQUEST_DEADLINE_MS; + return fromEnv ? Number(fromEnv) : 30_000; +} const REQUEST_ATTEMPT_TIMEOUT_MS = 5_000; @@ -120,13 +127,31 @@ export class CouchChanges extends EventEmitter { */ public async attachAllMetadata( changes: readonly DatabaseChange[] - ): Promise { - const outcomes = await Promise.all( - changes.map((change) => this.attachMetadata(change)) - ); - return outcomes.filter( - (outcome): outcome is AttachedChange => outcome !== undefined + ): Promise { + const ok = new Array(); + const failedSeqs = new Array(); + await Promise.all( + changes.map(async (change) => { + try { + const outcome = await this.attachMetadata(change); + if (outcome !== undefined) { + ok.push(outcome); + } + } catch (error) { + // One unreachable packument must not fail the whole batch. The + // entry is reported as failed so the caller does not record a + // receipt for it; a later scan retries it. + console.error( + `Failed to fetch metadata for ${change.id}, a later scan will retry it: ${error}` + ); + const seq = Number(change.seq); + if (!isNaN(seq)) { + failedSeqs.push(seq); + } + } + }) ); + return { ok, failedSeqs }; } private async attachMetadata( @@ -219,7 +244,7 @@ export class CouchChanges extends EventEmitter { timeout: REQUEST_ATTEMPT_TIMEOUT_MS, }; - const deadline = Date.now() + REQUEST_DEADLINE_MS; + const deadline = Date.now() + requestDeadlineMs(); let maxDelay = 100; while (true) { try { @@ -301,7 +326,13 @@ function readResponseJson( class RetryableError extends Error {} function isRetryableError(e: Error): boolean { - return e instanceof RetryableError || (e as any).code === 'ECONNRESET'; + return ( + e instanceof RetryableError || + (e as any).code === 'ECONNRESET' || + // A truncated response body (the gunzip stream ends unexpectedly). Seen + // repeatedly from registry.npmjs.org; transient like a connection reset. + (e as any).code === 'Z_BUF_ERROR' + ); } async function sleep(ms: number) { @@ -352,6 +383,22 @@ export interface DatabaseChanges { readonly totalCount: number; } +/** + * The result of attaching registry metadata to a batch of change entries. + */ +export interface AttachedMetadata { + /** + * The change entries whose metadata could be fetched. + */ + readonly ok: AttachedChange[]; + + /** + * The sequence numbers of change entries whose metadata fetch failed. No + * receipt must be recorded for these, so that a later scan retries them. + */ + readonly failedSeqs: number[]; +} + /** * A change entry with its registry metadata attached. */ diff --git a/src/package-sources/npmjs/npm-js-follower.lambda.ts b/src/package-sources/npmjs/npm-js-follower.lambda.ts index a9ca3acd3..47d2ba68d 100644 --- a/src/package-sources/npmjs/npm-js-follower.lambda.ts +++ b/src/package-sources/npmjs/npm-js-follower.lambda.ts @@ -161,51 +161,56 @@ export async function handler(event: ScheduledEvent, context: Context) { state.addCheckpoint(head); } - // Re-check laggy packuments (packages whose registry packument was behind - // the revision announced by the feed when we first saw them). - const laggyStaged = await retryLaggyPackuments( - context, - npm, - state, - stagingFunction, - denyList, - licenseList, - knownVersions - ); - writeKnownVersionsFile ||= laggyStaged > 0; - - // Scan the trailing window of the feed. - const scanStaged = await runScan( - context, - npm, - state, - head, - stagingFunction, - denyList, - licenseList, - knownVersions - ); - writeKnownVersionsFile ||= scanStaged > 0; - - // The laggy packument gauge is emitted at the end of the run, so it - // reflects the expectations recorded by this run's scan (emitting it during - // the retry phase would always read the trough: after recoveries, before - // new expectations are recorded). - await metricScope((metrics) => async () => { - metrics.setDimensions({}); - metrics.putMetric( - MetricName.LAGGY_PACKUMENTS, - state.laggyPackumentCount, - Unit.Count + try { + // Re-check laggy packuments (packages whose registry packument was behind + // the revision announced by the feed when we first saw them). + const laggyStaged = await retryLaggyPackuments( + context, + npm, + state, + stagingFunction, + denyList, + licenseList, + knownVersions ); - })(); + writeKnownVersionsFile ||= laggyStaged > 0; + + // Scan the trailing window of the feed. + const scanStaged = await runScan( + context, + npm, + state, + head, + stagingFunction, + denyList, + licenseList, + knownVersions + ); + writeKnownVersionsFile ||= scanStaged > 0; - await Promise.all([ - saveFollowerState(context, stagingBucket, state), - ...(writeKnownVersionsFile - ? [saveLastKnownVersions(context, stagingBucket, knownVersions)] - : []), - ]); + // The laggy packument gauge is emitted at the end of the run, so it + // reflects the expectations recorded by this run's scan (emitting it during + // the retry phase would always read the trough: after recoveries, before + // new expectations are recorded). + await metricScope((metrics) => async () => { + metrics.setDimensions({}); + metrics.putMetric( + MetricName.LAGGY_PACKUMENTS, + state.laggyPackumentCount, + Unit.Count + ); + })(); + } finally { + // Persist the state even when a scan failed part-way: receipts, interim + // checkpoints, and staged known versions represent completed work that + // the next run should not redo. + await Promise.all([ + saveFollowerState(context, stagingBucket, state), + ...(writeKnownVersionsFile + ? [saveLastKnownVersions(context, stagingBucket, knownVersions)] + : []), + ]); + } console.log('All done here, we have success!'); @@ -364,7 +369,7 @@ async function processBatch( ); } - const attached = await npm.attachAllMetadata(fresh); + const { ok: attached, failedSeqs } = await npm.attachAllMetadata(fresh); // The most recent "modified" timestamp observed in the batch. let lastModified: Date | undefined; @@ -423,8 +428,11 @@ async function processBatch( } // Record receipts for every entry received (including deleted or - // unreachable packages: they were received and deliberately skipped). - state.addSeqs(batch.seqs); + // unpublished packages: they were received and deliberately skipped) - + // except entries whose metadata fetch failed, so a later scan retries + // them. + const failed = new Set(failedSeqs); + state.addSeqs(batch.seqs.filter((seq) => !failed.has(seq))); } finally { metrics.putMetric(MetricName.LAST_SEQ, head, Unit.None); metrics.putMetric( From 24085cd5e1e72354c344605b8d18d5360ab737c4 Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Thu, 24 Sep 2026 09:34:18 +0100 Subject: [PATCH 2/3] chore: address review feedback Removes the env-overridable request deadline again: tests now use HTTP 403 responses (client errors are not retried), so no test seam is needed. Adds a MetadataFetchFailures metric for entries whose packument fetch failed after retries, wired into the Feed Reliability widget. --- .../__snapshots__/construct-hub.test.ts.snap | 20 +++++++++--------- .../__snapshots__/snapshot.test.ts.snap | 4 ++-- .../npmjs/npm-js-follower.lambda.test.ts | 19 ++++++++++------- src/package-sources/npmjs.ts | 21 +++++++++++++++++++ .../npmjs/constants.lambda-shared.ts | 7 +++++++ .../npmjs/couch-changes.lambda-shared.ts | 11 ++-------- .../npmjs/npm-js-follower.lambda.ts | 5 +++++ 7 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/__tests__/__snapshots__/construct-hub.test.ts.snap b/src/__tests__/__snapshots__/construct-hub.test.ts.snap index d1115220e..7454e2805 100644 --- a/src/__tests__/__snapshots__/construct-hub.test.ts.snap +++ b/src/__tests__/__snapshots__/construct-hub.test.ts.snap @@ -711,7 +711,7 @@ def sort_filter_rules(json_obj): { "Ref": "AWS::Region", }, - "","metrics":[[{"label":"Late Changes","expression":"FILL(m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeCount",{"label":"Late Changes","stat":"Sum","visible":false,"id":"m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5"}],[{"label":"Laggy Packuments","expression":"FILL(m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackuments",{"label":"Laggy Packuments","stat":"Maximum","visible":false,"id":"m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc"}],[{"label":"Laggy Packuments Recovered","expression":"FILL(meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentsRecovered",{"label":"Laggy Packuments Recovered","stat":"Sum","visible":false,"id":"meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7"}],[{"label":"Laggy Packument Give-Ups","expression":"FILL(m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentGiveUps",{"label":"Laggy Packument Give-Ups","stat":"Sum","visible":false,"id":"m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeLag",{"label":"Late Change Lag (max)","stat":"Maximum","yAxis":"right"}]],"annotations":{"horizontal":[{"color":"#ff0000","label":"Scan Window","value":21600000,"yAxis":"right"}]},"yAxis":{"left":{"min":0},"right":{"label":"Milliseconds","min":0,"showUnits":false}},"period":300}},{"type":"metric","width":12,"height":6,"x":0,"y":77,"properties":{"view":"timeSeries","title":"Stager Dead-Letter Queue","region":"", + "","metrics":[[{"label":"Late Changes","expression":"FILL(m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeCount",{"label":"Late Changes","stat":"Sum","visible":false,"id":"m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5"}],[{"label":"Laggy Packuments","expression":"FILL(m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackuments",{"label":"Laggy Packuments","stat":"Maximum","visible":false,"id":"m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc"}],[{"label":"Laggy Packuments Recovered","expression":"FILL(meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentsRecovered",{"label":"Laggy Packuments Recovered","stat":"Sum","visible":false,"id":"meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7"}],[{"label":"Laggy Packument Give-Ups","expression":"FILL(m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentGiveUps",{"label":"Laggy Packument Give-Ups","stat":"Sum","visible":false,"id":"m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665"}],[{"label":"Metadata Fetch Failures","expression":"FILL(mb4ba2ab8081f9474c7f13c3726c05a8d8243b0432c76e4ce05db82f5f80bf5bb, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","MetadataFetchFailures",{"label":"Metadata Fetch Failures","stat":"Sum","visible":false,"id":"mb4ba2ab8081f9474c7f13c3726c05a8d8243b0432c76e4ce05db82f5f80bf5bb"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeLag",{"label":"Late Change Lag (max)","stat":"Maximum","yAxis":"right"}]],"annotations":{"horizontal":[{"color":"#ff0000","label":"Scan Window","value":21600000,"yAxis":"right"}]},"yAxis":{"left":{"min":0},"right":{"label":"Milliseconds","min":0,"showUnits":false}},"period":300}},{"type":"metric","width":12,"height":6,"x":0,"y":77,"properties":{"view":"timeSeries","title":"Stager Dead-Letter Queue","region":"", { "Ref": "AWS::Region", }, @@ -9974,7 +9974,7 @@ RunBook: https://github.com/cdklabs/construct-hub/blob/main/docs/operator-runboo "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "1387dd527fc81c8e9e360d21d3c6c29f9121c41ff623ea54b4f28ccedcfc567a.zip", + "S3Key": "d58fed793fe662f0b62aaceaede6cf0fc861f804fd2c8236871b3eecb5c18567.zip", }, "Description": "[Test/ConstructHub/Sources/NpmJs] Periodically query npmjs.com index for new packages", "Environment": { @@ -14790,7 +14790,7 @@ def sort_filter_rules(json_obj): { "Ref": "AWS::Region", }, - "","metrics":[[{"label":"Late Changes","expression":"FILL(m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeCount",{"label":"Late Changes","stat":"Sum","visible":false,"id":"m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5"}],[{"label":"Laggy Packuments","expression":"FILL(m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackuments",{"label":"Laggy Packuments","stat":"Maximum","visible":false,"id":"m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc"}],[{"label":"Laggy Packuments Recovered","expression":"FILL(meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentsRecovered",{"label":"Laggy Packuments Recovered","stat":"Sum","visible":false,"id":"meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7"}],[{"label":"Laggy Packument Give-Ups","expression":"FILL(m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentGiveUps",{"label":"Laggy Packument Give-Ups","stat":"Sum","visible":false,"id":"m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeLag",{"label":"Late Change Lag (max)","stat":"Maximum","yAxis":"right"}]],"annotations":{"horizontal":[{"color":"#ff0000","label":"Scan Window","value":21600000,"yAxis":"right"}]},"yAxis":{"left":{"min":0},"right":{"label":"Milliseconds","min":0,"showUnits":false}},"period":300}},{"type":"metric","width":12,"height":6,"x":0,"y":77,"properties":{"view":"timeSeries","title":"Stager Dead-Letter Queue","region":"", + "","metrics":[[{"label":"Late Changes","expression":"FILL(m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeCount",{"label":"Late Changes","stat":"Sum","visible":false,"id":"m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5"}],[{"label":"Laggy Packuments","expression":"FILL(m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackuments",{"label":"Laggy Packuments","stat":"Maximum","visible":false,"id":"m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc"}],[{"label":"Laggy Packuments Recovered","expression":"FILL(meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentsRecovered",{"label":"Laggy Packuments Recovered","stat":"Sum","visible":false,"id":"meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7"}],[{"label":"Laggy Packument Give-Ups","expression":"FILL(m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentGiveUps",{"label":"Laggy Packument Give-Ups","stat":"Sum","visible":false,"id":"m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665"}],[{"label":"Metadata Fetch Failures","expression":"FILL(mb4ba2ab8081f9474c7f13c3726c05a8d8243b0432c76e4ce05db82f5f80bf5bb, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","MetadataFetchFailures",{"label":"Metadata Fetch Failures","stat":"Sum","visible":false,"id":"mb4ba2ab8081f9474c7f13c3726c05a8d8243b0432c76e4ce05db82f5f80bf5bb"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeLag",{"label":"Late Change Lag (max)","stat":"Maximum","yAxis":"right"}]],"annotations":{"horizontal":[{"color":"#ff0000","label":"Scan Window","value":21600000,"yAxis":"right"}]},"yAxis":{"left":{"min":0},"right":{"label":"Milliseconds","min":0,"showUnits":false}},"period":300}},{"type":"metric","width":12,"height":6,"x":0,"y":77,"properties":{"view":"timeSeries","title":"Stager Dead-Letter Queue","region":"", { "Ref": "AWS::Region", }, @@ -24427,7 +24427,7 @@ RunBook: https://github.com/cdklabs/construct-hub/blob/main/docs/operator-runboo "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "1387dd527fc81c8e9e360d21d3c6c29f9121c41ff623ea54b4f28ccedcfc567a.zip", + "S3Key": "d58fed793fe662f0b62aaceaede6cf0fc861f804fd2c8236871b3eecb5c18567.zip", }, "Description": "[Test/ConstructHub/Sources/NpmJs] Periodically query npmjs.com index for new packages", "Environment": { @@ -29181,7 +29181,7 @@ def sort_filter_rules(json_obj): { "Ref": "AWS::Region", }, - "","metrics":[[{"label":"Late Changes","expression":"FILL(m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeCount",{"label":"Late Changes","stat":"Sum","visible":false,"id":"m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5"}],[{"label":"Laggy Packuments","expression":"FILL(m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackuments",{"label":"Laggy Packuments","stat":"Maximum","visible":false,"id":"m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc"}],[{"label":"Laggy Packuments Recovered","expression":"FILL(meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentsRecovered",{"label":"Laggy Packuments Recovered","stat":"Sum","visible":false,"id":"meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7"}],[{"label":"Laggy Packument Give-Ups","expression":"FILL(m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentGiveUps",{"label":"Laggy Packument Give-Ups","stat":"Sum","visible":false,"id":"m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeLag",{"label":"Late Change Lag (max)","stat":"Maximum","yAxis":"right"}]],"annotations":{"horizontal":[{"color":"#ff0000","label":"Scan Window","value":21600000,"yAxis":"right"}]},"yAxis":{"left":{"min":0},"right":{"label":"Milliseconds","min":0,"showUnits":false}},"period":300}},{"type":"metric","width":12,"height":6,"x":0,"y":77,"properties":{"view":"timeSeries","title":"Stager Dead-Letter Queue","region":"", + "","metrics":[[{"label":"Late Changes","expression":"FILL(m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeCount",{"label":"Late Changes","stat":"Sum","visible":false,"id":"m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5"}],[{"label":"Laggy Packuments","expression":"FILL(m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackuments",{"label":"Laggy Packuments","stat":"Maximum","visible":false,"id":"m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc"}],[{"label":"Laggy Packuments Recovered","expression":"FILL(meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentsRecovered",{"label":"Laggy Packuments Recovered","stat":"Sum","visible":false,"id":"meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7"}],[{"label":"Laggy Packument Give-Ups","expression":"FILL(m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentGiveUps",{"label":"Laggy Packument Give-Ups","stat":"Sum","visible":false,"id":"m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665"}],[{"label":"Metadata Fetch Failures","expression":"FILL(mb4ba2ab8081f9474c7f13c3726c05a8d8243b0432c76e4ce05db82f5f80bf5bb, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","MetadataFetchFailures",{"label":"Metadata Fetch Failures","stat":"Sum","visible":false,"id":"mb4ba2ab8081f9474c7f13c3726c05a8d8243b0432c76e4ce05db82f5f80bf5bb"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeLag",{"label":"Late Change Lag (max)","stat":"Maximum","yAxis":"right"}]],"annotations":{"horizontal":[{"color":"#ff0000","label":"Scan Window","value":21600000,"yAxis":"right"}]},"yAxis":{"left":{"min":0},"right":{"label":"Milliseconds","min":0,"showUnits":false}},"period":300}},{"type":"metric","width":12,"height":6,"x":0,"y":77,"properties":{"view":"timeSeries","title":"Stager Dead-Letter Queue","region":"", { "Ref": "AWS::Region", }, @@ -38444,7 +38444,7 @@ RunBook: https://github.com/cdklabs/construct-hub/blob/main/docs/operator-runboo "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "1387dd527fc81c8e9e360d21d3c6c29f9121c41ff623ea54b4f28ccedcfc567a.zip", + "S3Key": "d58fed793fe662f0b62aaceaede6cf0fc861f804fd2c8236871b3eecb5c18567.zip", }, "Description": "[Test/ConstructHub/Sources/NpmJs] Periodically query npmjs.com index for new packages", "Environment": { @@ -43346,7 +43346,7 @@ def sort_filter_rules(json_obj): { "Ref": "AWS::Region", }, - "","metrics":[[{"label":"Late Changes","expression":"FILL(m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeCount",{"label":"Late Changes","stat":"Sum","visible":false,"id":"m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5"}],[{"label":"Laggy Packuments","expression":"FILL(m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackuments",{"label":"Laggy Packuments","stat":"Maximum","visible":false,"id":"m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc"}],[{"label":"Laggy Packuments Recovered","expression":"FILL(meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentsRecovered",{"label":"Laggy Packuments Recovered","stat":"Sum","visible":false,"id":"meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7"}],[{"label":"Laggy Packument Give-Ups","expression":"FILL(m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentGiveUps",{"label":"Laggy Packument Give-Ups","stat":"Sum","visible":false,"id":"m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeLag",{"label":"Late Change Lag (max)","stat":"Maximum","yAxis":"right"}]],"annotations":{"horizontal":[{"color":"#ff0000","label":"Scan Window","value":21600000,"yAxis":"right"}]},"yAxis":{"left":{"min":0},"right":{"label":"Milliseconds","min":0,"showUnits":false}},"period":300}},{"type":"metric","width":12,"height":6,"x":0,"y":77,"properties":{"view":"timeSeries","title":"Stager Dead-Letter Queue","region":"", + "","metrics":[[{"label":"Late Changes","expression":"FILL(m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeCount",{"label":"Late Changes","stat":"Sum","visible":false,"id":"m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5"}],[{"label":"Laggy Packuments","expression":"FILL(m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackuments",{"label":"Laggy Packuments","stat":"Maximum","visible":false,"id":"m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc"}],[{"label":"Laggy Packuments Recovered","expression":"FILL(meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentsRecovered",{"label":"Laggy Packuments Recovered","stat":"Sum","visible":false,"id":"meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7"}],[{"label":"Laggy Packument Give-Ups","expression":"FILL(m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentGiveUps",{"label":"Laggy Packument Give-Ups","stat":"Sum","visible":false,"id":"m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665"}],[{"label":"Metadata Fetch Failures","expression":"FILL(mb4ba2ab8081f9474c7f13c3726c05a8d8243b0432c76e4ce05db82f5f80bf5bb, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","MetadataFetchFailures",{"label":"Metadata Fetch Failures","stat":"Sum","visible":false,"id":"mb4ba2ab8081f9474c7f13c3726c05a8d8243b0432c76e4ce05db82f5f80bf5bb"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeLag",{"label":"Late Change Lag (max)","stat":"Maximum","yAxis":"right"}]],"annotations":{"horizontal":[{"color":"#ff0000","label":"Scan Window","value":21600000,"yAxis":"right"}]},"yAxis":{"left":{"min":0},"right":{"label":"Milliseconds","min":0,"showUnits":false}},"period":300}},{"type":"metric","width":12,"height":6,"x":0,"y":77,"properties":{"view":"timeSeries","title":"Stager Dead-Letter Queue","region":"", { "Ref": "AWS::Region", }, @@ -52618,7 +52618,7 @@ RunBook: https://github.com/cdklabs/construct-hub/blob/main/docs/operator-runboo "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "1387dd527fc81c8e9e360d21d3c6c29f9121c41ff623ea54b4f28ccedcfc567a.zip", + "S3Key": "d58fed793fe662f0b62aaceaede6cf0fc861f804fd2c8236871b3eecb5c18567.zip", }, "Description": "[Test/ConstructHub/Sources/NpmJs] Periodically query npmjs.com index for new packages", "Environment": { @@ -57696,7 +57696,7 @@ def sort_filter_rules(json_obj): { "Ref": "AWS::Region", }, - "","metrics":[[{"label":"Late Changes","expression":"FILL(m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeCount",{"label":"Late Changes","stat":"Sum","visible":false,"id":"m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5"}],[{"label":"Laggy Packuments","expression":"FILL(m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackuments",{"label":"Laggy Packuments","stat":"Maximum","visible":false,"id":"m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc"}],[{"label":"Laggy Packuments Recovered","expression":"FILL(meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentsRecovered",{"label":"Laggy Packuments Recovered","stat":"Sum","visible":false,"id":"meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7"}],[{"label":"Laggy Packument Give-Ups","expression":"FILL(m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentGiveUps",{"label":"Laggy Packument Give-Ups","stat":"Sum","visible":false,"id":"m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeLag",{"label":"Late Change Lag (max)","stat":"Maximum","yAxis":"right"}]],"annotations":{"horizontal":[{"color":"#ff0000","label":"Scan Window","value":21600000,"yAxis":"right"}]},"yAxis":{"left":{"min":0},"right":{"label":"Milliseconds","min":0,"showUnits":false}},"period":300}},{"type":"metric","width":12,"height":6,"x":0,"y":77,"properties":{"view":"timeSeries","title":"Stager Dead-Letter Queue","region":"", + "","metrics":[[{"label":"Late Changes","expression":"FILL(m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeCount",{"label":"Late Changes","stat":"Sum","visible":false,"id":"m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5"}],[{"label":"Laggy Packuments","expression":"FILL(m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackuments",{"label":"Laggy Packuments","stat":"Maximum","visible":false,"id":"m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc"}],[{"label":"Laggy Packuments Recovered","expression":"FILL(meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentsRecovered",{"label":"Laggy Packuments Recovered","stat":"Sum","visible":false,"id":"meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7"}],[{"label":"Laggy Packument Give-Ups","expression":"FILL(m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentGiveUps",{"label":"Laggy Packument Give-Ups","stat":"Sum","visible":false,"id":"m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665"}],[{"label":"Metadata Fetch Failures","expression":"FILL(mb4ba2ab8081f9474c7f13c3726c05a8d8243b0432c76e4ce05db82f5f80bf5bb, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","MetadataFetchFailures",{"label":"Metadata Fetch Failures","stat":"Sum","visible":false,"id":"mb4ba2ab8081f9474c7f13c3726c05a8d8243b0432c76e4ce05db82f5f80bf5bb"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeLag",{"label":"Late Change Lag (max)","stat":"Maximum","yAxis":"right"}]],"annotations":{"horizontal":[{"color":"#ff0000","label":"Scan Window","value":21600000,"yAxis":"right"}]},"yAxis":{"left":{"min":0},"right":{"label":"Milliseconds","min":0,"showUnits":false}},"period":300}},{"type":"metric","width":12,"height":6,"x":0,"y":77,"properties":{"view":"timeSeries","title":"Stager Dead-Letter Queue","region":"", { "Ref": "AWS::Region", }, @@ -66775,7 +66775,7 @@ RunBook: https://github.com/cdklabs/construct-hub/blob/main/docs/operator-runboo "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "1387dd527fc81c8e9e360d21d3c6c29f9121c41ff623ea54b4f28ccedcfc567a.zip", + "S3Key": "d58fed793fe662f0b62aaceaede6cf0fc861f804fd2c8236871b3eecb5c18567.zip", }, "Description": "[Test/ConstructHub/Sources/NpmJs] Periodically query npmjs.com index for new packages", "Environment": { diff --git a/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap b/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap index 7ec886c78..e07f1424f 100644 --- a/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap +++ b/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap @@ -773,7 +773,7 @@ def sort_filter_rules(json_obj): { "Ref": "AWS::Region", }, - "","metrics":[[{"label":"Late Changes","expression":"FILL(m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeCount",{"label":"Late Changes","stat":"Sum","visible":false,"id":"m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5"}],[{"label":"Laggy Packuments","expression":"FILL(m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackuments",{"label":"Laggy Packuments","stat":"Maximum","visible":false,"id":"m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc"}],[{"label":"Laggy Packuments Recovered","expression":"FILL(meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentsRecovered",{"label":"Laggy Packuments Recovered","stat":"Sum","visible":false,"id":"meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7"}],[{"label":"Laggy Packument Give-Ups","expression":"FILL(m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentGiveUps",{"label":"Laggy Packument Give-Ups","stat":"Sum","visible":false,"id":"m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeLag",{"label":"Late Change Lag (max)","stat":"Maximum","yAxis":"right"}]],"annotations":{"horizontal":[{"color":"#ff0000","label":"Scan Window","value":21600000,"yAxis":"right"}]},"yAxis":{"left":{"min":0},"right":{"label":"Milliseconds","min":0,"showUnits":false}},"period":300}},{"type":"metric","width":12,"height":6,"x":0,"y":77,"properties":{"view":"timeSeries","title":"Stager Dead-Letter Queue","region":"", + "","metrics":[[{"label":"Late Changes","expression":"FILL(m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeCount",{"label":"Late Changes","stat":"Sum","visible":false,"id":"m97b26d9c3912f8122fc8cbc26c10d7e362f10a1b01dd7b6fc615793a0614ffd5"}],[{"label":"Laggy Packuments","expression":"FILL(m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackuments",{"label":"Laggy Packuments","stat":"Maximum","visible":false,"id":"m7542c42c2a42804f45673f1b7a1e208ef9657546e83a7fed66558c0c9a9458fc"}],[{"label":"Laggy Packuments Recovered","expression":"FILL(meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentsRecovered",{"label":"Laggy Packuments Recovered","stat":"Sum","visible":false,"id":"meebc873175632650e3dbd407c0e22f4395c7b945fffbd66f17cc225c7f3c5ab7"}],[{"label":"Laggy Packument Give-Ups","expression":"FILL(m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","LaggyPackumentGiveUps",{"label":"Laggy Packument Give-Ups","stat":"Sum","visible":false,"id":"m25c8aa905abbcebe79405fb8d308d3b26cc11789e67b7b432396024cc3688665"}],[{"label":"Metadata Fetch Failures","expression":"FILL(mb4ba2ab8081f9474c7f13c3726c05a8d8243b0432c76e4ce05db82f5f80bf5bb, 0)"}],["ConstructHub/PackageSource/NpmJs/Follower","MetadataFetchFailures",{"label":"Metadata Fetch Failures","stat":"Sum","visible":false,"id":"mb4ba2ab8081f9474c7f13c3726c05a8d8243b0432c76e4ce05db82f5f80bf5bb"}],["ConstructHub/PackageSource/NpmJs/Follower","LateChangeLag",{"label":"Late Change Lag (max)","stat":"Maximum","yAxis":"right"}]],"annotations":{"horizontal":[{"color":"#ff0000","label":"Scan Window","value":21600000,"yAxis":"right"}]},"yAxis":{"left":{"min":0},"right":{"label":"Milliseconds","min":0,"showUnits":false}},"period":300}},{"type":"metric","width":12,"height":6,"x":0,"y":77,"properties":{"view":"timeSeries","title":"Stager Dead-Letter Queue","region":"", { "Ref": "AWS::Region", }, @@ -11249,7 +11249,7 @@ Direct link to the function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "1387dd527fc81c8e9e360d21d3c6c29f9121c41ff623ea54b4f28ccedcfc567a.zip", + "S3Key": "d58fed793fe662f0b62aaceaede6cf0fc861f804fd2c8236871b3eecb5c18567.zip", }, "Description": "[dev/ConstructHub/Sources/NpmJs] Periodically query npmjs.com index for new packages", "Environment": { diff --git a/src/__tests__/package-sources/npmjs/npm-js-follower.lambda.test.ts b/src/__tests__/package-sources/npmjs/npm-js-follower.lambda.test.ts index bdeb2e97b..36e57d225 100644 --- a/src/__tests__/package-sources/npmjs/npm-js-follower.lambda.test.ts +++ b/src/__tests__/package-sources/npmjs/npm-js-follower.lambda.test.ts @@ -121,7 +121,6 @@ afterEach(() => { delete process.env[ENV_DENY_LIST_OBJECT_KEY]; delete process.env[LicenseListEnv.BUCKET_NAME]; delete process.env[LicenseListEnv.OBJECT_KEY]; - delete process.env.REQUEST_DEADLINE_MS; nock.cleanAll(); }); @@ -567,7 +566,6 @@ test('retries truncated gzip responses from the registry', async () => { }); test('does not receipt entries whose metadata fetch failed, so a later scan retries them', async () => { - process.env.REQUEST_DEADLINE_MS = '0'; // do not wait out transient retries givenHead(1_010); givenNoStateFile(); givenLegacyMarker(1_000); @@ -592,7 +590,9 @@ test('does not receipt entries whose metadata fetch failed, so a later scan retr ], last_seq: 1_006, }); - nock(REGISTRY).get('/broken-package').reply(500, 'oops'); + // A client error is used so the request is not retried until the + // transient-error deadline. + nock(REGISTRY).get('/broken-package').reply(403, 'forbidden'); nock(REGISTRY) .get('/good-package') .reply(200, packageDoc('good-package', '1-bbbbbb', '1.0.0')); @@ -607,21 +607,26 @@ test('does not receipt entries whose metadata fetch failed, so a later scan retr const state = savedState(); expect(state.has(1_006)).toBe(true); expect(state.has(1_004)).toBe(false); + expect(mockPutMetric).toHaveBeenCalledWith( + MetricName.METADATA_FETCH_FAILURES, + 1, + Unit.Count + ); }); test('persists its state even when the scan fails part-way', async () => { - process.env.REQUEST_DEADLINE_MS = '0'; givenHead(1_010); givenNoStateFile(); givenLegacyMarker(1_000); - // The feed itself is unavailable: the scan throws. + // The feed itself rejects the request: the scan throws. (A client error + // is used so the request is not retried until the transient-error deadline.) nock(REPLICA) .get('/registry/_changes') .query({ limit: '10000', since: '1000' }) - .reply(503, 'unavailable'); + .reply(403, 'forbidden'); - await expect(handler(event, context)).rejects.toThrow(/HTTP 503/); + await expect(handler(event, context)).rejects.toThrow(/HTTP 403/); // The state (here: the seeded checkpoint) was saved regardless. expect(savedState().newestCheckpointSeq()).toBe(1_000); diff --git a/src/package-sources/npmjs.ts b/src/package-sources/npmjs.ts index 0c60da3a7..c11b3aa71 100644 --- a/src/package-sources/npmjs.ts +++ b/src/package-sources/npmjs.ts @@ -392,6 +392,12 @@ export class NpmJs implements IPackageSource { }), 0 ), + fillMetric( + this.metricMetadataFetchFailures({ + label: 'Metadata Fetch Failures', + }), + 0 + ), ], leftYAxis: { min: 0 }, right: [ @@ -522,6 +528,21 @@ export class NpmJs implements IPackageSource { }); } + /** + * The number of change entries whose registry metadata could not be fetched + * (after transient-error retries). These entries are not receipted, so a + * later scan retries them. + */ + public metricMetadataFetchFailures(opts?: MetricOptions): Metric { + return new Metric({ + period: Duration.minutes(5), + statistic: Stats.SUM, + ...opts, + metricName: MetricName.METADATA_FETCH_FAILURES, + namespace: METRICS_NAMESPACE, + }); + } + /** * The number of packages for which the registry packument is still behind * the revision announced by the `_changes` feed. The versions served so far diff --git a/src/package-sources/npmjs/constants.lambda-shared.ts b/src/package-sources/npmjs/constants.lambda-shared.ts index caf354cd8..ea5f09ef6 100644 --- a/src/package-sources/npmjs/constants.lambda-shared.ts +++ b/src/package-sources/npmjs/constants.lambda-shared.ts @@ -32,6 +32,13 @@ export const enum MetricName { */ LATE_CHANGE_LAG = 'LateChangeLag', + /** + * Number of change entries whose registry metadata could not be fetched + * (after transient-error retries). These entries are not receipted, so a + * later scan retries them. + */ + METADATA_FETCH_FAILURES = 'MetadataFetchFailures', + /** * The number of packages for which the registry packument is still behind * the revision announced by the `_changes` feed ("laggy packuments"). The diff --git a/src/package-sources/npmjs/couch-changes.lambda-shared.ts b/src/package-sources/npmjs/couch-changes.lambda-shared.ts index d28ae2128..243ba742b 100644 --- a/src/package-sources/npmjs/couch-changes.lambda-shared.ts +++ b/src/package-sources/npmjs/couch-changes.lambda-shared.ts @@ -8,14 +8,7 @@ import { createGunzip } from 'zlib'; const NPM_REGISTRY_URL = 'https://registry.npmjs.org/'; -/** - * How long to keep retrying transient request failures. Can be overridden - * through the environment for testing purposes. - */ -function requestDeadlineMs(): number { - const fromEnv = process.env.REQUEST_DEADLINE_MS; - return fromEnv ? Number(fromEnv) : 30_000; -} +const REQUEST_DEADLINE_MS = 30_000; const REQUEST_ATTEMPT_TIMEOUT_MS = 5_000; @@ -244,7 +237,7 @@ export class CouchChanges extends EventEmitter { timeout: REQUEST_ATTEMPT_TIMEOUT_MS, }; - const deadline = Date.now() + requestDeadlineMs(); + const deadline = Date.now() + REQUEST_DEADLINE_MS; let maxDelay = 100; while (true) { try { diff --git a/src/package-sources/npmjs/npm-js-follower.lambda.ts b/src/package-sources/npmjs/npm-js-follower.lambda.ts index 47d2ba68d..3c1c5ddaf 100644 --- a/src/package-sources/npmjs/npm-js-follower.lambda.ts +++ b/src/package-sources/npmjs/npm-js-follower.lambda.ts @@ -370,6 +370,11 @@ async function processBatch( } const { ok: attached, failedSeqs } = await npm.attachAllMetadata(fresh); + metrics.putMetric( + MetricName.METADATA_FETCH_FAILURES, + failedSeqs.length, + Unit.Count + ); // The most recent "modified" timestamp observed in the batch. let lastModified: Date | undefined; From 8c758f34579784a72329c09277eed1bd08c4bb3e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:37:47 +0000 Subject: [PATCH 3/3] chore: self mutation Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- API.md | 18 ++++++++++++++++++ .../__snapshots__/construct-hub.test.ts.snap | 10 +++++----- .../devapp/__snapshots__/snapshot.test.ts.snap | 2 +- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/API.md b/API.md index c15fb177e..628b9e841 100644 --- a/API.md +++ b/API.md @@ -2886,6 +2886,7 @@ new sources.NpmJs(props?: NpmJsProps) | metricLastSeq | The last sequence number that was processed. | | metricLateChangeCount | The number of change entries discovered by a scan that were inserted into the `_changes` feed behind a position the follower had already read past. | | metricLateChangeLag | For each late change entry, the time elapsed between the moment the follower first read past the entry's sequence number and the moment the entry was discovered. | +| metricMetadataFetchFailures | The number of change entries whose registry metadata could not be fetched (after transient-error retries). | | metricNpmJsChangeAge | *No description.* | | metricPackageVersionAge | The age of the oldest package version that was processed. | | metricPackageVersionCount | The total count of package versions that were inspected. | @@ -3040,6 +3041,23 @@ and can be used to tune the scan window. --- +##### `metricMetadataFetchFailures` + +```typescript +public metricMetadataFetchFailures(opts?: MetricOptions): Metric +``` + +The number of change entries whose registry metadata could not be fetched (after transient-error retries). + +These entries are not receipted, so a +later scan retries them. + +###### `opts`Optional + +- *Type:* aws-cdk-lib.aws_cloudwatch.MetricOptions + +--- + ##### `metricNpmJsChangeAge` ```typescript diff --git a/src/__tests__/__snapshots__/construct-hub.test.ts.snap b/src/__tests__/__snapshots__/construct-hub.test.ts.snap index 7454e2805..5e3412976 100644 --- a/src/__tests__/__snapshots__/construct-hub.test.ts.snap +++ b/src/__tests__/__snapshots__/construct-hub.test.ts.snap @@ -10859,7 +10859,7 @@ Direct link to Lambda function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "9949a6fc6d8cf1e0891cc64e355de47a97027d870030596dc77d3f6915e603f5.zip", + "S3Key": "4ffbb107d5c61355d7d0b2e1675dc33fa6ff3617685ff538e3a1e6cd07136f29.zip", }, "DeadLetterConfig": { "TargetArn": { @@ -25312,7 +25312,7 @@ Direct link to Lambda function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "9949a6fc6d8cf1e0891cc64e355de47a97027d870030596dc77d3f6915e603f5.zip", + "S3Key": "4ffbb107d5c61355d7d0b2e1675dc33fa6ff3617685ff538e3a1e6cd07136f29.zip", }, "DeadLetterConfig": { "TargetArn": { @@ -39329,7 +39329,7 @@ Direct link to Lambda function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "9949a6fc6d8cf1e0891cc64e355de47a97027d870030596dc77d3f6915e603f5.zip", + "S3Key": "4ffbb107d5c61355d7d0b2e1675dc33fa6ff3617685ff538e3a1e6cd07136f29.zip", }, "DeadLetterConfig": { "TargetArn": { @@ -53490,7 +53490,7 @@ Direct link to Lambda function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "9949a6fc6d8cf1e0891cc64e355de47a97027d870030596dc77d3f6915e603f5.zip", + "S3Key": "4ffbb107d5c61355d7d0b2e1675dc33fa6ff3617685ff538e3a1e6cd07136f29.zip", }, "DeadLetterConfig": { "TargetArn": { @@ -67660,7 +67660,7 @@ Direct link to Lambda function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "9949a6fc6d8cf1e0891cc64e355de47a97027d870030596dc77d3f6915e603f5.zip", + "S3Key": "4ffbb107d5c61355d7d0b2e1675dc33fa6ff3617685ff538e3a1e6cd07136f29.zip", }, "DeadLetterConfig": { "TargetArn": { diff --git a/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap b/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap index e07f1424f..a985a1ac9 100644 --- a/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap +++ b/src/__tests__/devapp/__snapshots__/snapshot.test.ts.snap @@ -12128,7 +12128,7 @@ Direct link to Lambda function: /lambda/home#/functions/", "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", }, - "S3Key": "9949a6fc6d8cf1e0891cc64e355de47a97027d870030596dc77d3f6915e603f5.zip", + "S3Key": "4ffbb107d5c61355d7d0b2e1675dc33fa6ff3617685ff538e3a1e6cd07136f29.zip", }, "DeadLetterConfig": { "TargetArn": {