Repository: serverless/dashboard (language: python)

Issue:
Fix request and response hooks handling
_Reported internally by @Mmarzex_

It appears that AWS Lambda instrumentation supports only _sync_ request and response hooks, and eventual promises as returned by them are not awaited

It was not that problematic for lamdbas which resolved with a _callback_ and over there `context.callbackWaitsForEmptyEventLoop` is set to `true`, so AWS naturally waited for that hook to resolve before settling the invocation. Still when result of lambda is _thenable_ this setting is set to _false_ and AWS immediately settles the invocation.

With this PR I patched handling of responses so in all cases we wait for hooks to resolve. Additionally I've ensured we handle the right way the edge case of callback/thenable race (where given lambda is attempted to be resolved with both callback and thenable).

I've also added as simple test script that allows to invoke internal extension locally (for debugging purposes)




Discussion / hints:

Fix patch:
diff --git a/node/package.json b/node/package.json
index 5f898f57e..60036caff 100644
--- a/node/package.json
+++ b/node/package.json
@@ -15,9 +15,10 @@
     ]
   },
   "devDependencies": {
+    "@aws-sdk/client-cloudwatch-logs": "^3.105.0",
     "@aws-sdk/client-iam": "^3.100.0",
     "@aws-sdk/client-lambda": "^3.100.0",
-    "@aws-sdk/client-s3": "^3.100.0",
+    "@aws-sdk/client-sts": "^3.105.0",
     "@commitlint/cli": "^17.0.2",
     "@commitlint/lint": "^17.0.0",
     "@serverless/eslint-config": "^4.0.1",
@@ -36,13 +37,15 @@
     "lint-staged": "^12.5.0",
     "lodash": "^4.17.21",
     "log": "^6.3.1",
+    "log-node": "^8.0.3",
+    "memoizee": "^0.4.15",
     "mocha": "^9.2.2",
     "ncjsm": "^4.3.0",
     "node-fetch": "^2.6.7",
+    "node-machine-id": "^1.1.12",
     "prettier": "^2.6.2",
     "process-utils": "^4.0.0",
     "standard-version": "^9.5.0",
-    "stream-promise": "^3.2.0",
     "timers-ext": "^0.1.7",
     "type": "^2.6.0",
     "uuid": "^8.3.2"
diff --git a/node/packages/aws-lambda-otel-extension/README.md b/node/packages/aws-lambda-otel-extension/README.md
index a3e8fca0a..289255862 100644
--- a/node/packages/aws-lambda-otel-extension/README.md
+++ b/node/packages/aws-lambda-otel-extension/README.md
@@ -156,7 +156,9 @@ In tests, the home folder is mocked, therefore AWS access cannot be reliably set
 
 Easiest is to run tests is by setting `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_REGION` environment variables.
 
-Tests create a temporary layer, S3, and Lambda resources and remove them after the test is finalized.
+Tests create a temporary layer and Lambda resources and remove them after the test is finalized.
+
+All created resourced are named or prefixed with `test-otel-extension-<testUid>` string, where `testUid` is four characters taken from [local machine id](https://www.npmjs.com/package/node-machine-id) or in case of CI runs a random string. `testUid` string can also be overriden via environment variable `TEST_UID`
 
 ```bash
 npm run test:integration
diff --git a/node/packages/aws-lambda-otel-extension/external/otel-extension-external/index.js b/node/packages/aws-lambda-otel-extension/external/otel-extension-external/index.js
index d2b990494..30728b815 100644
--- a/node/packages/aws-lambda-otel-extension/external/otel-extension-external/index.js
+++ b/node/packages/aws-lambda-otel-extension/external/otel-extension-external/index.js
@@ -96,6 +96,7 @@ module.exports = (async () => {
             for (const event of data) {
               switch (event.type) {
                 case 'platform.start':
+                  debugLog('Extension platform log: start');
                   getCurrentRequestData('start');
                   // eslint-disable-next-line no-loop-func
                   ongoingInvocationDeferred = new Promise((resolve) => {
@@ -103,19 +104,23 @@ module.exports = (async () => {
                   });
                   break;
                 case 'platform.runtimeDone':
+                  debugLog('Extension platform log: runtimeDone');
                   invocationStartTime = process.hrtime.bigint();
                   runtimeEventEmitter.emit('runtimeDone');
                   if (event.record.status === 'success') continue;
                   // In case of invocation failure extension will be shutdown before generating report
                   if (lastTelemetryData) {
                     fs.writeFileSync(tmpStorageFile, JSON.stringify(lastTelemetryData));
+                    debugLog('Extension: Store telemetry data for the next process');
                   }
                   closeOngoingInvocation();
                   break;
                 case 'platform.report':
+                  debugLog('Extension platform log: report');
                   if (!lastTelemetryData) {
                     try {
                       lastTelemetryData = JSON.parse(fs.readFileSync(tmpStorageFile), 'utf-8');
+                      debugLog('Extension: Restore telemetry data from the previous process');
                     } catch {
                       /* ignore */
                     }
@@ -184,6 +189,7 @@ module.exports = (async () => {
     const waitUntilAllReportsAreSent = () => Promise.all(Array.from(pendingReports));
     // Events lifecycle handler
     const waitForEvent = async () => {
+      debugLog('Extension: Ready for an event');
       const event = await new Promise((resolve, reject) => {
         if (isInitializing) {
           isInitializing = false;
@@ -224,6 +230,7 @@ module.exports = (async () => {
         request.on('error', reject);
         request.end();
       });
+      debugLog('Extension: Received event ', event.eventType);
       switch (event.eventType) {
         case 'SHUTDOWN':
           shutdownStartTime = process.hrtime.bigint();
diff --git a/node/packages/aws-lambda-otel-extension/external/otel-extension-external/report-otel-data.js b/node/packages/aws-lambda-otel-extension/external/otel-extension-external/report-otel-data.js
index a3b0dfeb1..3f3796b47 100644
--- a/node/packages/aws-lambda-otel-extension/external/otel-extension-external/report-otel-data.js
+++ b/node/packages/aws-lambda-otel-extension/external/otel-extension-external/report-otel-data.js
@@ -113,8 +113,6 @@ module.exports = async (name, data) => {
       s3: { key: `${process.env.AWS_LAMBDA_FUNCTION_NAME}/metrics/${new Date().toISOString()}` },
     });
   } else {
-    process.stdout.write(`⚡ ${name}: `);
-    process.stdout.write(JSON.stringify(data));
-    process.stdout.write('\n');
+    process.stdout.write(`⚡ ${name}: ${JSON.stringify(data)}\n`);
   }
 };
diff --git a/node/packages/aws-lambda-otel-extension/internal/otel-extension-internal-node/index.js b/node/packages/aws-lambda-otel-extension/internal/otel-extension-internal-node/index.js
index 4f6b971ee..35e9b1c6f 100644
--- a/node/packages/aws-lambda-otel-extension/internal/otel-extension-internal-node/index.js
+++ b/node/packages/aws-lambda-otel-extension/internal/otel-extension-internal-node/index.js
@@ -2,6 +2,13 @@
 
 const processStartTime = process.hrtime.bigint();
 
+const debugLog = (...args) => {
+  if (process.env.DEBUG_SLS_OTEL_LAYER) {
+    process._rawDebug(...args);
+  }
+};
+debugLog('Internal extension: Init');
+
 const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
 const { InMemorySpanExporter } = require('@opentelemetry/sdk-trace-base');
 const { registerInstrumentations } = require('@opentelemetry/instrumentation');
@@ -115,6 +122,7 @@ const requestHandler = async (span, { event, context }) => {
       `${require('util').inspect(eventDataPayload, { depth: Infinity, colors: true })}\n`
     );
   } else {
+    debugLog('Internal extension: Send event data');
     // Send request data to external so that we can attach this data to logs
     await fetch(`http://localhost:${OTEL_SERVER_PORT}`, {
       method: 'post',
@@ -338,6 +346,7 @@ const responseHandler = async (span, { res, err }, isTimeout) => {
       `${require('util').inspect(telemetryDataPayload, { depth: Infinity, colors: true })}\n`
     );
   } else {
+    debugLog('Internal extension: Send telemetry data');
     await fetch(`http://localhost:${OTEL_SERVER_PORT}`, {
       method: 'post',
       body: JSON.stringify(telemetryDataPayload),
@@ -352,11 +361,9 @@ const responseHandler = async (span, { res, err }, isTimeout) => {
 };
 
 const handleTimeouts = (remainingTime) => {
-  const timeoutTime = remainingTime - 50;
-  // const setTimeoutTime = Date.now();
-  timeoutHandler = setTimeout(async () => {
-    await responseHandler(null, {}, true);
-  }, timeoutTime).unref();
+  timeoutHandler = setTimeout(() => {
+    responseHandler(null, {}, true);
+  }, remainingTime - 50).unref();
 };
 
 registerInstrumentations({
diff --git a/node/packages/aws-lambda-otel-extension/internal/otel-extension-internal-node/wrapper.js b/node/packages/aws-lambda-otel-extension/internal/otel-extension-internal-node/wrapper.js
index 590639df5..0ed3485cc 100644
--- a/node/packages/aws-lambda-otel-extension/internal/otel-extension-internal-node/wrapper.js
+++ b/node/packages/aws-lambda-otel-extension/internal/otel-extension-internal-node/wrapper.js
@@ -19,16 +19,18 @@ delete EvalError.$serverlessAwsLambdaInstrumentation;
 let requestStartTime;
 let responseStartTime;
 let currentInvocationId = 0;
+const debugLog = (...args) => {
+  if (process.env.DEBUG_SLS_OTEL_LAYER) process._rawDebug(...args);
+};
 
 const wrappedHandler = awsLambdaInstrumentation._instance._getPatchHandler(
   (event, context, callback) => {
     const invocationId = currentInvocationId;
-    if (process.env.DEBUG_SLS_OTEL_LAYER) {
-      process._rawDebug(
-        'Extension overhead duration: internal request:',
-        `${Math.round(Number(process.hrtime.bigint() - requestStartTime) / 1000000)}ms`
-      );
-    }
+    debugLog(
+      'Extension overhead duration: internal request:',
+      `${Math.round(Number(process.hrtime.bigint() - requestStartTime) / 1000000)}ms`
+    );
+
     const wrapCallback =
       (originalCallback) =>
       (...args) => {
@@ -54,14 +56,15 @@ const wrappedHandler = awsLambdaInstrumentation._instance._getPatchHandler(
 );
 
 module.exports.handler = (event, context, callback) => {
+  debugLog('Internal extension: Invocation');
   const invocationId = ++currentInvocationId;
   requestStartTime = process.hrtime.bigint();
   const logResponseDuration = () => {
     if (invocationId !== currentInvocationId) return;
     delete EvalError.$serverlessRequestHandlerPromise;
     delete EvalError.$serverlessResponseHandlerPromise;
-    if (process.env.DEBUG_SLS_OTEL_LAYER && responseStartTime) {
-      process._rawDebug(
+    if (responseStartTime) {
+      debugLog(
         'Extension overhead duration: internal response:',
         `${Math.round(Number(process.hrtime.bigint() - responseStartTime) / 1000000)}ms`
       );
diff --git a/node/packages/aws-lambda-otel-extension/scripts/lib/ensure-npm-dependencies.js b/node/packages/aws-lambda-otel-extension/scripts/lib/ensure-npm-dependencies.js
index 123c16885..830d732a2 100644
--- a/node/packages/aws-lambda-otel-extension/scripts/lib/ensure-npm-dependencies.js
+++ b/node/packages/aws-lambda-otel-extension/scripts/lib/ensure-npm-dependencies.js
@@ -2,36 +2,40 @@
 
 const path = require('path');
 const semver = require('semver');
+const memoizee = require('memoizee');
 
 const rootPath = path.resolve(__dirname, '../../');
 
-module.exports = (dirname) => {
-  const pkgJson = require(path.resolve(rootPath, dirname, 'package'));
-  for (const [dependencyName, dependencyVersionRange] of Object.entries({
-    ...(pkgJson.devDependencies || {}),
-    ...(pkgJson.dependencies || {}),
-  })) {
-    const dependencyVersion = (() => {
-      try {
-        return require(path.resolve(
-          rootPath,
-          dirname,
-          'node_modules',
-          dependencyName,
-          'package.json'
-        )).version;
-      } catch {
+module.exports = memoizee(
+  (dirname) => {
+    const pkgJson = require(path.resolve(rootPath, dirname, 'package'));
+    for (const [dependencyName, dependencyVersionRange] of Object.entries({
+      ...(pkgJson.devDependencies || {}),
+      ...(pkgJson.dependencies || {}),
+    })) {
+      const dependencyVersion = (() => {
+        try {
+          return require(path.resolve(
+            rootPath,
+            dirname,
+            'node_modules',
+            dependencyName,
+            'package.json'
+          )).version;
+        } catch {
+          throw new Error(
+            `Outdated dependencies state at "${dirname}" ("${dependencyName}" is not installed). ` +
+              'Please run "npm install"'
+          );
+        }
+      })();
+      if (!semver.satisfies(dependencyVersion, dependencyVersionRange)) {
         throw new Error(
-          `Outdated dependencies state at "${dirname}" ("${dependencyName}" is not installed). ` +
+          `Outdated dependencies state at "${dirname}" (unexpected version of "${dependencyName}" ). ` +
             'Please run "npm install"'
         );
       }
-    })();
-    if (!semver.satisfies(dependencyVersion, dependencyVersionRange)) {
-      throw new Error(
-        `Outdated dependencies state at "${dirname}" (unexpected version of "${dependencyName}" ). ` +
-          'Please run "npm install"'
-      );
     }
-  }
-};
+  },
+  { primitive: true }
+);


Test patch:
diff --git a/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/.eslintrc.js b/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/.eslintrc.js
index f6e255b1a..f71b67c7e 100644
--- a/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/.eslintrc.js
+++ b/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/.eslintrc.js
@@ -17,5 +17,5 @@ module.exports = {
       },
     ],
   },
-  ignorePatterns: ['esbuild-esm-callback-success.js'],
+  ignorePatterns: ['success-callback-esbuild-from-esm.js'],
 };
diff --git a/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/esbuild-esm-callback-success.js b/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/success-callback-esbuild-from-esm.js
similarity index 100%
rename from node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/esbuild-esm-callback-success.js
rename to node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/success-callback-esbuild-from-esm.js
diff --git a/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/esm-callback-success/.eslintrc.cjs b/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/success-callback-esm/.eslintrc.cjs
similarity index 100%
rename from node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/esm-callback-success/.eslintrc.cjs
rename to node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/success-callback-esm/.eslintrc.cjs
diff --git a/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/esm-callback-success/index.js b/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/success-callback-esm/index.js
similarity index 100%
rename from node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/esm-callback-success/index.js
rename to node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/success-callback-esm/index.js
diff --git a/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/esm-callback-success/package.json b/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/success-callback-esm/package.json
similarity index 100%
rename from node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/esm-callback-success/package.json
rename to node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/success-callback-esm/package.json
diff --git a/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/express-app.js b/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/success-callback-express.js
similarity index 100%
rename from node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/express-app.js
rename to node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/success-callback-express.js
diff --git a/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/callback-success.js b/node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/success-callback.js
similarity index 100%
rename from node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/callback-success.js
rename to node/packages/aws-lambda-otel-extension/test/fixtures/lambdas/success-callback.js
diff --git a/node/packages/aws-lambda-otel-extension/test/integration/aws-request.js b/node/packages/aws-lambda-otel-extension/test/integration/aws-request.js
new file mode 100644
index 000000000..30dd499e0
--- /dev/null
+++ b/node/packages/aws-lambda-otel-extension/test/integration/aws-request.js
@@ -0,0 +1,8 @@
+'use strict';
+
+const awsRequest = require('@serverless/test/aws-request');
+
+const awsClientParams = { region: process.env.AWS_REGION };
+
+module.exports = (client, method, args) =>
+  awsRequest({ client, params: awsClientParams }, method, args);
diff --git a/node/packages/aws-lambda-otel-extension/test/integration/basename.js b/node/packages/aws-lambda-otel-extension/test/integration/basename.js
new file mode 100644
index 000000000..bbb02bb31
--- /dev/null
+++ b/node/packages/aws-lambda-otel-extension/test/integration/basename.js
@@ -0,0 +1,5 @@
+'use strict';
+
+const testUid = require('./test-uid');
+
+module.exports = `test-otel-extension-${testUid}`;
diff --git a/node/packages/aws-lambda-otel-extension/test/integration/cleanup.js b/node/packages/aws-lambda-otel-extension/test/integration/cleanup.js
new file mode 100644
index 000000000..de58747bd
--- /dev/null
+++ b/node/packages/aws-lambda-otel-extension/test/integration/cleanup.js
@@ -0,0 +1,90 @@
+'use strict';
+
+const { IAM } = require('@aws-sdk/client-iam');
+const { Lambda } = require('@aws-sdk/client-lambda');
+const { STS } = require('@aws-sdk/client-sts');
+const log = require('log').get('test');
+const awsRequest = require('./aws-request');
+const basename = require('./basename');
+
+const getAllFunctionNames = async (marker = undefined) => {
+  const result = await awsRequest(Lambda, 'listFunctions', {
+    FunctionVersion: 'ALL',
+    Marker: marker,
+  });
+  const functionNames = result.Functions.map(
+    ({ FunctionName: functionName }) => functionName
+  ).filter((functionName) => functionName.startsWith(`${basename}-`));
+
+  if (result.NextMarker) {
+    functionNames.push(...Array.from(await getAllFunctionNames(result.NextMarker)));
+  }
+  return new Set(functionNames);
+};
+
+const deleteFunctions = async () => {
+  await Promise.all(
+    Array.from(await getAllFunctionNames(), async (functionName) => {
+      await awsRequest(Lambda, 'deleteFunction', { FunctionName: functionName });
+      log.notice('Deleted function %s', functionName);
+    })
+  );
+};
+
+const deleteLayers = async () => {
+  const layerVersions = (await awsRequest(Lambda, 'listLayerVersions', { LayerName: basename }))
+    .LayerVersions;
+  if (!layerVersions.length) return;
+
+  const lastLayerVersion = layerVersions[0].Version;
+  await Promise.all(
+    Array.from(Array(lastLayerVersion).keys()).map(async (index) => {
+      const versionNumber = index + 1;
+      await awsRequest(Lambda, 'deleteLayerVersion', {
+        LayerName: basename,
+        VersionNumber: versionNumber,
+      });
+    })
+  );
+
+  log.notice('Deleted all versions of layer %s', basename);
+};
+
+const deleteRole = async () => {
+  const policyArn = `arn:aws:iam::${
+    (await awsRequest(STS, 'getCallerIdentity')).Account
+  }:policy/${basename}`;
+
+  try {
+    await awsRequest(IAM, 'detachRolePolicy', { RoleName: basename, PolicyArn: policyArn });
+    log.notice('Detached IAM policy %s from %s', policyArn, basename);
+  } catch (error) {
+    if (error.Code !== 'NoSuchEntity') throw error;
+  }
+  await Promise.all([
+    awsRequest(IAM, 'deleteRole', { RoleName: basename }).then(
+      () => {
+        log.notice('Deleted IAM role %s', basename);
+      },
+      (error) => {
+        if (error.Code === 'NoSuchEntity') return;
+        throw error;
+      }
+    ),
+    awsRequest(IAM, 'deletePolicy', { PolicyArn: policyArn }).then(
+      () => {
+        log.notice('Deleted IAM policy %s', policyArn);
+      },
+      (error) => {
+        if (error.Code === 'NoSuchEntity') return;
+        throw error;
+      }
+    ),
+  ]);
+};
+
+module.exports = async (options = {}) => {
+  log.notice('Cleanup %s', basename);
+  if (!options.skipFunctionsCleanup) await deleteFunctions();
+  await Promise.all([deleteLayers(), deleteRole()]);
+};
diff --git a/node/packages/aws-lambda-otel-extension/test/integration/create-core-resources.js b/node/packages/aws-lambda-otel-extension/test/integration/create-core-resources.js
new file mode 100644
index 000000000..66d82b495
--- /dev/null
+++ b/node/packages/aws-lambda-otel-extension/test/integration/create-core-resources.js
@@ -0,0 +1,99 @@
+'use strict';
+
+const path = require('path');
+const fsp = require('fs').promises;
+const { Lambda } = require('@aws-sdk/client-lambda');
+const { IAM } = require('@aws-sdk/client-iam');
+const { STS } = require('@aws-sdk/client-sts');
+const log = require('log').get('test');
+const buildLayer = require('../../scripts/lib/build');
+const awsRequest = require('./aws-request');
+const basename = require('./basename');
+
+const layerFilename = path.resolve(__dirname, '../../dist/extension.zip');
+
+const createLayer = async (config) => {
+  if (!process.env.TEST_LAYER_FILENAME) {
+    log.info('Building layer');
+    await buildLayer(layerFilename);
+  }
+
+  log.info('Publishing layer (%s) to AWS', process.env.TEST_LAYER_FILENAME || layerFilename);
+  await awsRequest(Lambda, 'publishLayerVersion', {
+    LayerName: basename,
+    Content: { ZipFile: await fsp.readFile(process.env.TEST_LAYER_FILENAME || layerFilename) },
+  });
+  log.info('Resolving layer ARN');
+  config.layerArn = (
+    await awsRequest(Lambda, 'listLayerVersions', { LayerName: basename })
+  ).LayerVersions.shift().LayerVersionArn;
+  log.info('Layer ready %s', config.layerArn);
+};
+
+const createRole = async (config) => {
+  log.info('Creating IAM role and policy');
+  const [
+    {
+      Role: { Arn: roleArn },
+    },
+    {
+      Policy: { Arn: policyArn },
+    },
+  ] = await Promise.all([
+    awsRequest(IAM, 'createRole', {
+      RoleName: basename,
+      AssumeRolePolicyDocument: JSON.stringify({
+        Version: '2012-10-17',
+        Statement: [
+          {
+            Effect: 'Allow',
+            Principal: { Service: ['lambda.amazonaws.com'] },
+            Action: ['sts:AssumeRole'],
+          },
+        ],
+      }),
+    }).catch((error) => {
+      if (error.Code === 'EntityAlreadyExists') {
+        log.notice('IAM role already exists');
+        return { Role: { Arn: `arn:aws:iam::${config.accountId}:role/${basename}` } };
+      }
+      throw error;
+    }),
+    awsRequest(IAM, 'createPolicy', {
+      PolicyName: basename,
+      PolicyDocument: JSON.stringify({
+        Version: '2012-10-17',
+        Statement: [
+          {
+            Effect: 'Allow',
+            Action: ['logs:CreateLogStream', 'logs:CreateLogGroup'],
+            Resource: `arn:*:logs:*:*:log-group:/aws/lambda/${basename}*:*`,
+          },
+          {
+            Effect: 'Allow',
+            Action: ['logs:PutLogEvents'],
+            Resource: `arn:*:logs:*:*:log-group:/aws/lambda/${basename}*:*:*`,
+          },
+        ],
+      }),
+    }).catch((error) => {
+      if (error.Code === 'EntityAlreadyExists') {
+        log.notice('IAM policy already exists');
+        return { Policy: { Arn: `arn:aws:iam::${config.accountId}:policy/${basename}` } };
+      }
+      throw error;
+    }),
+  ]);
+  log.info('Attaching IAM policy to role');
+  await awsRequest(IAM, 'attachRolePolicy', { RoleName: basename, PolicyArn: policyArn });
+  log.info('Attached IAM policy to role');
+  config.roleArn = roleArn;
+  config.policyArn = policyArn;
+};
+
+module.exports = async (config) => {
+  log.notice('Creating %s', basename);
+  config.accountId = (await awsRequest(STS, 'getCallerIdentity')).Account;
+
+  await Promise.all([createLayer(config), createRole(config)]);
+};
diff --git a/node/packages/aws-lambda-otel-extension/test/integration/index.test.js b/node/packages/aws-lambda-otel-extension/test/integration/index.test.js
index b7b247d8e..04e066e2f 100644
--- a/node/packages/aws-lambda-otel-extension/test/integration/index.test.js
+++ b/node/packages/aws-lambda-otel-extension/test/integration/index.test.js
@@ -3,44 +3,25 @@
 const { expect } = require('chai');
 
 const path = require('path');
-const fsp = require('fs').promises;
-const wait = require('timers-ext/promise/sleep');
-const awsRequestBare = require('@serverless/test/aws-request');
-const { S3 } = require('@aws-sdk/client-s3');
-const { Lambda } = require('@aws-sdk/client-lambda');
-const { IAM } = require('@aws-sdk/client-iam');
-const streamToPromise = require('stream-promise/to-promise');
 const log = require('log').get('test');
-const buildLayer = require('../../scripts/lib/build');
-const resolveDirZipBuffer = require('../utils/resolve-dir-zip-buffer');
 const normalizeOtelAttributes = require('../utils/normalize-otel-attributes');
-const ensureNpmDependencies = require('../../scripts/lib/ensure-npm-dependencies');
-
-const nameTimeBase = new Date(2022, 1, 17).getTime();
-const layerFilename = path.resolve(__dirname, '../../dist/extension.zip');
-const fixturesDirname = path.resolve(__dirname, '../fixtures/lambdas');
-const hasFailed = require('@serverless/test/has-failed');
-
-const awsClientParams = { region: process.env.AWS_REGION };
-const awsRequest = (client, method, args) =>
-  awsRequestBare({ client, params: awsClientParams }, method, args);
+const basename = require('./basename');
+const cleanup = require('./cleanup');
+const createCoreResources = require('./create-core-resources');
+const processFunction = require('./process-function');
 
 describe('integration', function () {
   this.timeout(120000);
-  let basename;
-  let layerArn;
-  let policyArn;
-  let lambdasCodeZipBuffer;
-  let roleArn;
+  const coreConfig = {};
 
   const functionsConfig = new Map([
-    ['callback-success', true],
-    ['esbuild-esm-callback-success', true],
-    ['esm-callback-success/index', true],
+    ['success-callback', true],
+    ['success-callback-esbuild-from-esm', true],
+    ['success-callback-esm/index', true],
     [
-      'express-app',
+      'success-callback-express',
       {
-        invocationOptions: {
+        i: {
           payload: {
             version: '2.0',
             routeKey: '$default',
@@ -98,7 +79,13 @@ describe('integration', function () {
     [
       'error-timeout',
       {
-        invocationOptions: { isFailure: true },
+        invokeOptions: { isFailure: true },
+        // On timeout re-initialization of external extension gets slow, and we observe that second
+        // invocation times out before actually lambda is initialized.
+        // This is either because currently our external extension is Node.js based,
+        // so has slow startup time, or it can be performance issue on AWS side.
+        // To ensure reliable result increase timeout, so we get second invocation correct
+        createOptions: { configuration: { Timeout: 7 } },
         test: ({ instrumentationSpans }) => {
           const { attributes } =
             instrumentationSpans['@opentelemetry/instrumentation-aws-lambda'][0];
@@ -108,277 +95,83 @@ describe('integration', function () {
     ],
   ]);
 
-  const processFunction = async (handlerModuleName, options = {}) => {
-    const payload = options.payload || { foo: 'bar' };
-    const functionBasename = handlerModuleName.includes(path.sep)
-      ? path.dirname(handlerModuleName)
-      : handlerModuleName;
-    const functionName = `${basename}-${functionBasename}`;
-    const ensureIsActive = async () => {
-      const {
-        Configuration: { State: state },
-      } = await awsRequest(Lambda, 'getFunction', { FunctionName: functionName });
-      if (state !== 'Active') await ensureIsActive();
-    };
-    const invokeFunction = async () => {
-      const result = await awsRequest(Lambda, 'invoke', {
-        FunctionName: functionName,
-        Payload: Buffer.from(JSON.stringify(payload), 'utf8'),
-      });
-      try {
-        const responsePayload = JSON.parse(Buffer.from(result.Payload));
-        log.debug('invoke payload %O', responsePayload);
-        log.debug('invoke parsed payload %O', JSON.parse(responsePayload.body));
-      } catch {
-        /* ignore */
-      }
-      if (result.FunctionError) {
-        if (options.isFailure) return;
-        throw new Error(`Invocation errored: ${result.FunctionError}`);
-      }
-    };
-    const deleteFunction = async () => {
-      await awsRequest(Lambda, 'deleteFunction', { FunctionName: functionName });
-    };
-
-    log.info('Ensure function is active %s', functionBasename);
-    await ensureIsActive();
-    log.info('Invoke function %s', functionBasename);
-    await invokeFunction();
-
-    let objects;
-    do {
-      log.info('Invoke function again %s', functionBasename);
-      await invokeFunction();
-      await wait(1000);
-      log.info('Retrieve list of generated S3 objects %s', functionBasename);
-      objects = ((await awsRequest(S3, 'listObjectsV2', { Bucket: basename })).Contents || [])
-        .map((object) => object.Key)
-        .filter((key) => key.startsWith(`${functionName}/`));
-    } while (objects.length < 3);
-
-    log.info('Delete function %s', functionBasename);
-    await deleteFunction();
-    log.info('Retrieve body of generated S3 objects %s', functionBasename);
-    return Promise.all(
-      objects.map(async (objectKey) => {
-        const result = JSON.parse(
-          String(
-            await streamToPromise(
-              (
-                await awsRequest(S3, 'getObject', { Bucket: basename, Key: objectKey })
-              ).Body
-            )
-          )
-        );
-        log.info('Report for %s: %o', objectKey, result);
-        return result;
-      })
-    );
-  };
+  let deferredResults;
 
   before(async () => {
-    ensureNpmDependencies('test/fixtures/lambdas');
-    basename = `test-otel-extension-${(Date.now() - nameTimeBase).toString(32)}`;
-    log.notice('Creating %s', basename);
-
-    const createBucket = async () => {
-      log.info('Creating bucket %s', basename);
-      await awsRequest(S3, 'createBucket', { Bucket: basename });
-      log.info('Created bucket %s', basename);
-    };
-    const createLayer = async () => {
-      if (!process.env.TEST_LAYER_FILENAME) {
-        log.info('Building layer');
-        await buildLayer(layerFilename);
-      }
-
-      log.info('Publishing layer (%s) to AWS', process.env.TEST_LAYER_FILENAME || layerFilename);
-      await awsRequest(Lambda, 'publishLayerVersion', {
-        LayerName: basename,
-        Content: { ZipFile: await fsp.readFile(process.env.TEST_LAYER_FILENAME || layerFilename) },
-      });
-      log.info('Resolving layer ARN');
-      layerArn = (
-        await awsRequest(Lambda, 'listLayerVersions', { LayerName: basename })
-      ).LayerVersions.shift().LayerVersionArn;
-      log.info('Layer ready');
-    };
-    const createRole = async () => {
-      log.info('Creating IAM role and policy');
-      [
-        {
-          Role: { Arn: roleArn },
-        },
-        {
-          Policy: { Arn: policyArn },
-        },
-      ] = await Promise.all([
-        awsRequest(IAM, 'createRole', {
-          RoleName: basename,
-          AssumeRolePolicyDocument: JSON.stringify({
-            Version: '2012-10-17',
-            Statement: [
-              {
-                Effect: 'Allow',
-                Principal: { Service: ['lambda.amazonaws.com'] },
-                Action: ['sts:AssumeRole'],
-              },
-            ],
-          }),
-        }),
-        awsRequest(IAM, 'createPolicy', {
-          PolicyName: basename,
-          PolicyDocument: JSON.stringify({
-            Version: '2012-10-17',
-            Statement: [
-              {
-                Effect: 'Allow',
-                Action: ['logs:CreateLogStream', 'logs:CreateLogGroup'],
-                Resource: `arn:*:logs:*:*:log-group:/aws/lambda/${basename}*:*`,
-              },
-              {
-                Effect: 'Allow',
-                Action: ['logs:PutLogEvents'],
-                Resource: `arn:*:logs:*:*:log-group:/aws/lambda/${basename}*:*:*`,
-              },
-              {
-                Effect: 'Allow',
-                Action: ['s3:PutObject'],
-                Resource: `arn:aws:s3:::${basename}/*`,
-              },
-            ],
-          }),
-        }),
-      ]);
-      log.info('Attaching IAM policy to role');
-      await awsRequest(IAM, 'attachRolePolicy', { RoleName: basename, PolicyArn: policyArn });
-      log.info('Attached IAM policy to role');
-    };
-
-    const createFunctions = async () => {
-      await Promise.all(
-        Array.from(functionsConfig.keys()).map(async function self(handlerModuleName) {
-          const functionBasename = handlerModuleName.includes(path.sep)
-            ? path.dirname(handlerModuleName)
-            : handlerModuleName;
-          const functionName = `${basename}-${functionBasename}`;
-          log.info('Create function %s', functionBasename);
-          try {
-            await awsRequest(Lambda, 'createFunction', {
-              Code: {
-                ZipFile: lambdasCodeZipBuffer,
-              },
-              FunctionName: functionName,
-              Handler: `${handlerModuleName}.handler`,
-              Role: roleArn,
-              Runtime: 'nodejs14.x',
-              Environment: {
-                Variables: {
-                  AWS_LAMBDA_EXEC_WRAPPER: '/opt/otel-extension-internal-node/exec-wrapper.sh',
-                  SLS_OTEL_USER_SETTINGS: JSON.stringify({
-                    metrics: { outputType: 'json', destination: `s3://${basename}` },
-                    traces: { outputType: 'json', destination: `s3://${basename}` },
-                  }),
-                  OTEL_LOG_LEVEL: 'ALL',
-                  DEBUG_SLS_OTEL_LAYER: '1',
-                },
-              },
-              Layers: [layerArn],
-            });
-          } catch (error) {
-            if (
-              error.message.includes(
-                'The role defined for the function cannot be assumed by Lambda'
-              ) ||
-              error.message.includes('because the KMS key is invalid for CreateGrant')
-            ) {
-              // Occassional race condition issue on AWS side, retry
-              await self(handlerModuleName);
-              return;
-            }
-            throw error;
-          }
-        })
-      );
-    };
-
-    [lambdasCodeZipBuffer] = await Promise.all([
-      resolveDirZipBuffer(fixturesDirname),
-      createBucket(),
-      createLayer(),
-      createRole(),
-    ]);
-    await createFunctions();
+    await createCoreResources(coreConfig);
+    deferredResults = new Map(
+      Array.from(functionsConfig, ([handlerModuleName, testConfig]) => [
+        handlerModuleName,
+        processFunction(handlerModuleName, testConfig, coreConfig).catch((error) => ({
+          // As we process result promises sequentially step by step in next turn, allowing them to
+          // reject will generate unhandled rejection.
+          // Therefore this scenario is converted to successuful { error } resolution
+          error,
+        })),
+      ])
+    );
   });
 
-  for (const [handlerModuleName, { invocationOptions, test }] of functionsConfig) {
-    const functionBasename = handlerModuleName.includes(path.sep)
+  for (const [handlerModuleName, { invokeOptions = {}, test }] of functionsConfig) {
+    const functionBasename = handlerModuleName.includes('/')
       ? path.dirname(handlerModuleName)
       : handlerModuleName;
-    describe(functionBasename, () => {
-      let reports;
-      before(async () => {
-        reports = await processFunction(handlerModuleName, invocationOptions);
-      });
-      it('test', () => {
-        const metricsReport = reports.find((report) => report.resourceMetrics);
-        const tracesReport = reports.find((report) => report.resourceSpans);
-        const resourceMetrics = normalizeOtelAttributes(
-          metricsReport.resourceMetrics[0].resource.attributes
-        );
-        expect(resourceMetrics['faas.name']).to.equal(`${basename}-${functionBasename}`);
-        const resourceSpans = normalizeOtelAttributes(
-          tracesReport.resourceSpans[0].resource.attributes
-        );
-        expect(resourceSpans['faas.name']).to.equal(`${basename}-${functionBasename}`);
 
-        const instrumentationSpans = {};
-        for (const {
-          instrumentationLibrary: { name },
-          spans,
-        } of tracesReport.resourceSpans[0].instrumentationLibrarySpans) {
-          instrumentationSpans[name] = spans.map((span) => ({
-            ...span,
-            attributes: normalizeOtelAttributes(span.attributes),
-          }));
-        }
-        log.debug('instrumentationSpans %o', instrumentationSpans);
-        if (test) {
-          test({
-            metricsReport,
-            tracesReport,
-            resourceMetrics,
-            resourceSpans,
-            instrumentationSpans,
-          });
-        }
-      });
+    it(functionBasename, async () => {
+      const functionResult = await deferredResults.get(handlerModuleName);
+      if (functionResult.error) throw functionResult.error;
+      const { reports } = functionResult;
+      if (!invokeOptions.isFailure) {
+        // Current timeout handling is unreliable, therefore do not attempt to confirm
+        // on all reports
+
+        // While reports should come in order as specified below,
+        // there were observed cases when it wasn't the case,
+        // e.g. telemetryData (response) was received before eventData (request)
+        expect(
+          reports.map((invocationReports) => invocationReports.map(([type]) => type).sort())
+        ).to.deep.equal([
+          ['request', 'response', 'metrics', 'traces'].sort(),
+          ['metrics', 'request', 'response', 'metrics', 'traces'].sort(),
+        ]);
+      }
+      const allInvocationReports = reports.flat();
+      const metricsReport = allInvocationReports.find(
+        ([reportType]) => reportType === 'metrics'
+      )[1];
+
+      const tracesReport = allInvocationReports.find(([reportType]) => reportType === 'traces')[1];
+      const resourceMetrics = normalizeOtelAttributes(
+        metricsReport.resourceMetrics[0].resource.attributes
+      );
+      expect(resourceMetrics['faas.name']).to.equal(`${basename}-${functionBasename}`);
+      const resourceSpans = normalizeOtelAttributes(
+        tracesReport.resourceSpans[0].resource.attributes
+      );
+      expect(resourceSpans['faas.name']).to.equal(`${basename}-${functionBasename}`);
+
+      const instrumentationSpans = {};
+      for (const {
+        instrumentationLibrary: { name },
+        spans,
+      } of tracesReport.resourceSpans[0].instrumentationLibrarySpans) {
+        instrumentationSpans[name] = spans.map((span) => ({
+          ...span,
+          attributes: normalizeOtelAttributes(span.attributes),
+        }));
+      }
+      log.debug('instrumentationSpans %o', instrumentationSpans);
+      if (test) {
+        test({
+          metricsReport,
+          tracesReport,
+          resourceMetrics,
+          resourceSpans,
+          instrumentationSpans,
+        });
+      }
     });
   }
 
-  after(async function () {
-    if (hasFailed(this.test.parent)) return; // Avoid cleanup
-    const deleteBucket = async () => {
-      const objects = (
-        (await awsRequest(S3, 'listObjectsV2', { Bucket: basename })).Contents || []
-      ).map((object) => ({
-        Key: object.Key,
-      }));
-      if (objects.length) {
-        await awsRequest(S3, 'deleteObjects', { Bucket: basename, Delete: { Objects: objects } });
-      }
-      await awsRequest(S3, 'deleteBucket', { Bucket: basename });
-    };
-    const deleteLayer = async () =>
-      awsRequest(Lambda, 'deleteLayerVersion', { LayerName: basename, VersionNumber: 1 });
-    const deleteRole = async () => {
-      await awsRequest(IAM, 'detachRolePolicy', { RoleName: basename, PolicyArn: policyArn });
-      return Promise.all([
-        awsRequest(IAM, 'deleteRole', { RoleName: basename }),
-        awsRequest(IAM, 'deletePolicy', { PolicyArn: policyArn }),
-      ]);
-    };
-    await Promise.all([deleteBucket(), deleteLayer(), deleteRole()]);
-  });
+  after(async () => cleanup({ skipFunctionsCleanup: true }));
 });
diff --git a/node/packages/aws-lambda-otel-extension/test/integration/process-function.js b/node/packages/aws-lambda-otel-extension/test/integration/process-function.js
new file mode 100644
index 000000000..a44af1cb7
--- /dev/null
+++ b/node/packages/aws-lambda-otel-extension/test/integration/process-function.js
@@ -0,0 +1,163 @@
+'use strict';
+
+const path = require('path');
+const log = require('log').get('test');
+const { CloudWatchLogs } = require('@aws-sdk/client-cloudwatch-logs');
+const { Lambda } = require('@aws-sdk/client-lambda');
+const wait = require('timers-ext/promise/sleep');
+const basename = require('./basename');
+const awsRequest = require('./aws-request');
+const resolveDirZipBuffer = require('../utils/resolve-dir-zip-buffer');
+const ensureNpmDependencies = require('../../scripts/lib/ensure-npm-dependencies');
+
+const fixturesDirname = path.resolve(__dirname, '../fixtures/lambdas');
+
+const create = async (fnConfig, coreConfig, testConfig) => {
+  const { createOptions = {} } = testConfig;
+  try {
+    await awsRequest(Lambda, 'createFunction', {
+      Handler: `${fnConfig.handlerModuleName}.handler`,
+      Role: coreConfig.roleArn,
+      Runtime: 'nodejs14.x',
+      ...createOptions.configuration,
+      Code: {
+        ZipFile: fnConfig.codeZipBuffer,
+      },
+      FunctionName: fnConfig.name,
+      Layers: [coreConfig.layerArn],
+      Environment: {
+        Variables: {
+          AWS_LAMBDA_EXEC_WRAPPER: '/opt/otel-extension-internal-node/exec-wrapper.sh',
+          DEBUG_SLS_OTEL_LAYER: '1',
+          SLS_OTEL_USER_SETTINGS: JSON.stringify({
+            metrics: { outputType: 'json' },
+            traces: { outputType: 'json' },
+          }),
+        },
+      },
+    });
+  } catch (error) {
+    if (
+      error.message.includes('The role defined for the function cannot be assumed by Lambda') ||
+      error.message.includes('because the KMS key is invalid for CreateGrant')
+    ) {
+      // Occassional race condition issue on AWS side, retry
+      await create(fnConfig, coreConfig, testConfig);
+      return;
+    }
+    if (error.message.includes('Function already exist')) {
+      log.notice('Function %s already exists, deleting and re-creating', fnConfig.basename);
+      await awsRequest(Lambda, 'deleteFunction', { FunctionName: fnConfig.name });
+      await create(fnConfig, coreConfig, testConfig);
+      return;
+    }
+    throw error;
+  }
+};
+
+const ensureIsActive = async (fnConfig) => {
+  const {
+    Configuration: { State: state },
+  } = await awsRequest(Lambda, 'getFunction', { FunctionName: fnConfig.name });
+  if (state !== 'Active') await ensureIsActive(fnConfig);
+};
+
+const invoke = async (fnConfig, testConfig) => {
+  // Local computer time might a bit out of sync. Relax time range by 3 seconds
+  fnConfig.invokeStartTime = Date.now() - 3000;
+  const { invokeOptions = {} } = testConfig;
+
+  const result = await awsRequest(Lambda, 'invoke', {
+    FunctionName: fnConfig.name,
+    Payload: Buffer.from(JSON.stringify(invokeOptions.payload || {}), 'utf8'),
+  });
+  try {
+    const responsePayload = JSON.parse(Buffer.from(result.Payload));
+    log.debug('invoke payload %O', responsePayload);
+    log.debug('invoke parsed payload %O', JSON.parse(responsePayload.body));
+  } catch {
+    /* ignore */
+  }
+  if (result.FunctionError) {
+    if (invokeOptions.isFailure) return;
+    throw new Error(`Invocation errored: ${result.FunctionError}`);
+  }
+};
+
+const deleteFunction = async (fnConfig) => {
+  await awsRequest(Lambda, 'deleteFunction', { FunctionName: fnConfig.name });
+};
+
+const retrieveReports = async (fnConfig) => {
+  const retrieveReportEvents = async () => {
+    try {
+      return (
+        await awsRequest(CloudWatchLogs, 'filterLogEvents', {
+          startTime: fnConfig.invokeStartTime,
+          logGroupName: `/aws/lambda/${fnConfig.name}`,
+        })
+      ).events;
+    } catch (error) {
+      if (error.name === 'ResourceNotFoundException') {
+        log.info('log group not ready, wait and retry %s', fnConfig.basename);
+        await wait(1000);
+        return retrieveReportEvents();
+      }
+      throw error;
+    }
+  };
+
+  let reports;
+  do {
+    const events = await retrieveReportEvents();
+    reports = [];
+    let currentInvocationReports = [];
+    for (const { message } of events) {
+      if (message.startsWith('⚡')) {
+        const reportType = message.slice(2, message.indexOf(':'));
+        if (reportType === 'logs') continue;
+        currentInvocationReports.push([
+          reportType,
+          JSON.parse(message.slice(message.indexOf(':') + 1).trim()),
+        ]);
+        continue;
+      }
+      if (message.startsWith('REPORT RequestId: ')) {
+        reports.push(currentInvocationReports);
+        currentInvocationReports = [];
+      }
+    }
+  } while (reports.length < 2);
+
+  log.info('Obtained reports %s %o', fnConfig.basename, reports);
+  return reports;
+};
+
+module.exports = async (handlerModuleName, testConfig, coreConfig) => {
+  ensureNpmDependencies('test/fixtures/lambdas');
+  const fnConfig = {
+    handlerModuleName,
+    basename: handlerModuleName.includes('/') ? path.dirname(handlerModuleName) : handlerModuleName,
+    codeZipBuffer: await resolveDirZipBuffer(fixturesDirname),
+  };
+  fnConfig.name = `${basename}-${fnConfig.basename}`;
+
+  log.info('Create function %s', fnConfig.basename);
+  await create(fnConfig, coreConfig, testConfig);
+
+  log.info('Ensure function is active %s', fnConfig.basename);
+  await ensureIsActive(fnConfig);
+
+  fnConfig.invokeStartTime = Date.now();
+  log.info('Invoke function #1 %s', fnConfig.basename);
+  await invoke(fnConfig, testConfig);
+
+  log.info('Invoke function #2 %s', fnConfig.basename);
+  await invoke(fnConfig, testConfig);
+
+  log.info('Delete function %s', fnConfig.basename);
+  await deleteFunction(fnConfig);
+
+  log.info('Retrieve list of written reports %s', fnConfig.basename);
+  return { reports: await retrieveReports(fnConfig) };
+};
diff --git a/node/packages/aws-lambda-otel-extension/test/integration/test-uid.js b/node/packages/aws-lambda-otel-extension/test/integration/test-uid.js
new file mode 100644
index 000000000..0ab1e2cd7
--- /dev/null
+++ b/node/packages/aws-lambda-otel-extension/test/integration/test-uid.js
@@ -0,0 +1,20 @@
+'use strict';
+
+const { machineIdSync: getMachineId } = require('node-machine-id');
+const log = require('log').get('test');
+
+const nameTimeBase = new Date(2022, 1, 17).getTime();
+
+const testUid = (() => {
+  if (process.env.TEST_UID) return process.env.TEST_UID;
+  switch (process.env.TEST_UID_MODE || (process.env.CI ? 'run' : 'machine')) {
+    case 'run':
+      return (Date.now() - nameTimeBase).toString(32);
+    default:
+      return getMachineId(true).split('-')[1];
+  }
+})();
+
+log.notice('test uid: %s', testUid);
+
+module.exports = testUid;
diff --git a/node/packages/aws-lambda-otel-extension/test/scripts/cleanup.js b/node/packages/aws-lambda-otel-extension/test/scripts/cleanup.js
new file mode 100755
index 000000000..dcb03d67b
--- /dev/null
+++ b/node/packages/aws-lambda-otel-extension/test/scripts/cleanup.js
@@ -0,0 +1,8 @@
+#!/usr/bin/env node
+
+'use strict';
+
+require('essentials');
+require('log-node')();
+
+require('../integration/cleanup')();
diff --git a/node/packages/aws-lambda-otel-extension/test/unit/internal/index.test.js b/node/packages/aws-lambda-otel-extension/test/unit/internal/index.test.js
index 78cb7a8e3..eea034a08 100644
--- a/node/packages/aws-lambda-otel-extension/test/unit/internal/index.test.js
+++ b/node/packages/aws-lambda-otel-extension/test/unit/internal/index.test.js
@@ -113,12 +113,12 @@ describe('internal', () => {
     ];
   });
 
-  it('should handle plain success invocation', async () => handleSuccess('callback-success'));
+  it('should handle plain success invocation', async () => handleSuccess('success-callback'));
   it('should handle esbuild ESM bundle result', async () =>
-    handleSuccess('esbuild-esm-callback-success'));
-  it('should handle ESM module', async () => handleSuccess('esm-callback-success/index'));
+    handleSuccess('success-callback-esbuild-from-esm'));
+  it('should handle ESM module', async () => handleSuccess('success-callback-esm/index'));
   it('should handle "express" handler', async () => {
-    const report = await handleSuccess('express-app', {
+    const report = await handleSuccess('success-callback-express', {
       version: '2.0',
       routeKey: '$default',
       rawPath: '/',
diff --git a/node/packages/aws-lambda-otel-extension/test/utils/resolve-dir-zip-buffer.js b/node/packages/aws-lambda-otel-extension/test/utils/resolve-dir-zip-buffer.js
index f7c246eff..0e1da2637 100644
--- a/node/packages/aws-lambda-otel-extension/test/utils/resolve-dir-zip-buffer.js
+++ b/node/packages/aws-lambda-otel-extension/test/utils/resolve-dir-zip-buffer.js
@@ -3,20 +3,24 @@
 const path = require('path');
 const readdir = require('fs2/readdir');
 const AdmZip = require('adm-zip');
+const memoizee = require('memoizee');
 const log = require('log').get('test');
 
-module.exports = async (functionRoot) => {
-  log.info('Start creating zip buffer %s', functionRoot);
-  const lambdaFiles = await readdir(functionRoot, { depth: Infinity, type: { file: true } });
-  const zip = new AdmZip();
+module.exports = memoizee(
+  async (functionRoot) => {
+    log.info('Start creating zip buffer %s', functionRoot);
+    const lambdaFiles = await readdir(functionRoot, { depth: Infinity, type: { file: true } });
+    const zip = new AdmZip();
 
-  for (const file of lambdaFiles) {
-    zip.addLocalFile(path.resolve(functionRoot, file), path.dirname(file));
-  }
+    for (const file of lambdaFiles) {
+      zip.addLocalFile(path.resolve(functionRoot, file), path.dirname(file));
+    }
 
-  try {
-    return zip.toBuffer();
-  } finally {
-    log.info('Zip buffer genereated %s', functionRoot);
-  }
-};
+    try {
+      return zip.toBuffer();
+    } finally {
+      log.info('Zip buffer generated %s', functionRoot);
+    }
+  },
+  { primitive: true, promise: true }
+);