Imported from zerodep/pino-applicationinsights (
AGENTS.md). Install upstream withnpx skills add zerodep/pino-applicationinsights. Copyright stays with the author.
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
npm test— runs the full pipeline:npm run build(pretest) → mocha againsttest/src/**/*.jsandtest/commonjs/**/*.cjs→npm run lint→npm run tsc→npm run test:md(posttest). Lint must always run after tests — never skip it, never reorder it. Don't paper over an SDK warning with an env var; fix the underlying nock setup.npm run lint—eslint . --cache && prettier . --check --cache.npm run tsc—tsc -p test: type-checks every test file (test/**/*, JS included viacheckJs) plussrc/and the example app against the publishedtypes/index.d.ts.test/globals.d.tsdeclares theexpectglobal registered bychai/register-expect.js. Tests that deliberately pass bad input use// @ts-expect-erroron the offending line; everything else is fixed with JSDoc annotations, never by looseningtest/tsconfig.json.npm run build— Rollup bundlessrc/index.js→lib/index.cjsandsrc/fake-applicationinsights.js→lib/fake-applicationinsights.cjs, thendts-buddyregeneratestypes/index.d.ts. Required before tests because the CommonJS test (test/commonjs/log-transport-test.cjs) imports the builtlib/.npm run cov:html/npm run test:lcov— coverage via c8 oversrc. The lcov output is uploaded to Coveralls in CI; see Coverage below.- Run a single test:
npx mocha test/src/compose-test.js(or--grep "pattern"). - Node
>=20required (package.jsonengines);.nvmrcpins Node 22 and CI runs 22, 24 and latest.applicationinsights@3.17+itself requires Node >= 22, documented in README → Node.js version. Mocha is launched with--experimental-test-module-mocks(set in.mocharc.jsonnode-option) so tests can callmock.module(...)fromnode:test.
Coverage
The Coveralls badge in README.md is a project USP and ships at 100% statements / 100% branches / 100% functions / 100% lines. Hold that bar:
- After any source change, run
npx c8 -n src -r text mochaand confirm every file still reports 100% across the table. If a new branch isn't exercised, add a test rather than ship the branch uncovered — even for "shouldn't happen" defensive guards. Seetest/src/fake-applicationinsights-test.js#constructor handles a TelemetryClient that lacks getStatsbeatand…dispatcher matcher returns false for an empty body…for examples of how to drive unreachable-in-practice paths (shadow prototype methods,fetchan empty body directly). - Coverage regressions are caught by Coveralls' own PR check after the lcov upload — do not add a client-side
c8 --check-coverageflag. The user has explicitly declined that approach.
Architecture
The package ships two independent entry points that share the same applicationinsights peer dep but are otherwise decoupled:
-
src/index.js— the pino transport.compose(opts, Transformation?)returns a stream built withpino-abstract-transportthat wires:pino source → TelemetryTransformation (objectMode Transform) → destination WritableThe destination is either (a) a
Writableconstructed around aTelemetryClientwhosetrackcallback is bound to the client (default:trackTraceAndException), or (b) the caller's ownopts.destinationstream (in which caseconnectionString/track/configare ignored — useful for tests and for piping into the FakeApplicationInsights client).TelemetryTransformation.convertToTelemetryparses the pino JSON line, maps pino numeric levels (30/40/50/60) to the SDK's severity enum (seeSeverityLevelselection in v2 / v3 dual support below), stripsignoreKeys(defaulthostname, pid, level, time, msg) plustagOverridesfromproperties, and wraps anyerrfield in the localExceptionclass. SubclassingTelemetryTransformationand passing it as the second arg tocomposeis the supported customization path. -
src/fake-applicationinsights.js—FakeApplicationInsights(connectionString)is a test helper that usesnockto intercept POSTs to the AI ingestion endpoint and resolve aCollectDataper matched telemetry type.expectTelemetryType(type)queues a single-envelope expectation;expect(count)queues a count-based collector. Internally a single persistent nock interceptor (the dispatcher) decodes each request body via the exportedextractTelemetryItemshelper and resolves all matching pending expectations from that one POST — required because the v3 SDK batches multiple telemetry items (e.g. trace + exception from a singlelogger.error) into one HTTP request, and the previous one-interceptor-per-expectdesign only matched the first item. A persistent fallback interceptor (registered alongside the dispatcher) replies200with{ itemsReceived, itemsAccepted, errors: [] }so trailing telemetry sent after all expectations are satisfied still gets a valid Breeze response — without it the SDK logsIngestion endpoint could not be reached. The dispatcher + fallback are installed lazily on the firstexpect…()call (not in the constructor) so an idle FAI leaves no nock state behind, andreset()removes only the interceptors this instance registered (nevernock.cleanAll()) so it can't trample on user-registered nock state. Each FAI tracks its own interceptor refs and usesnock.removeInterceptor(ref)for targeted cleanup.nockis anoptionalDependencies, only loaded by this entry point.
v2 / v3 dual support
The applicationinsights peer dep is >=2 <4. The library targets the v2 SDK and the v3 classic-API "shim" (applicationinsights@3 re-implemented over @azure/monitor-opentelemetry-exporter). The differences are confined to four exported helpers in the two entry points:
src/fake-applicationinsights.js#parseConnectionString— own parser forInstrumentationKey=…;IngestionEndpoint=…strings, needed because v3 droppedclient.config.endpointUrlfrom the public surface.src/index.js#applyClientConfig— disables statsbeat by callingclient.getStatsbeat().enable(false)when a real statsbeat instance is returned (v2). v3'sgetStatsbeat()returnsnull, so the call is a no-op there; disabling statsbeat on v3 is a caller responsibility, set via theAPPLICATION_INSIGHTS_NO_STATSBEAT=disableandAPPLICATIONINSIGHTS_STATSBEAT_DISABLED=trueenv vars before anyapplicationinsightsimport (the OTel exporter reads the former for network statsbeat and customer SDK stats, the latter for non-essential statsbeat) (seeexample/logger.js'sworker.envrecipe andtest/helpers/setup.jsfor examples). The library deliberately never mutatesprocess.env.src/fake-applicationinsights.js#extractTelemetryItems—FakeApplicationInsightsaccepts both wire encodings: v2's gzipped NDJSON (delivered as a hex string by nock) and v3'sapplication/jsonarray (parsed by nock into a JS value). Both formats hit the same/v2.1/trackpath, which is hardcoded asINGESTION_PATHNAME.src/index.js#SeverityLevel— picks the version-appropriate enum at module load:applicationinsights.Contracts?.SeverityLevel(v2 numericVerbose=0…Critical=4) →applicationinsights.KnownSeverityLevel(v3 string'Verbose'…'Critical') → numeric fallback for hand-rolled mocks. v3'strackTracetreats numeric0as falsy and silently defaults toInformation, so v3 has to be fed the string enum. The wire-levelseverityLevelfield ends up numeric for v2 and string for v3 — tests use a per-versionwireSeveritymap (seelog-transport-test.js).
Cross-version test layers
CI installs both versions side-by-side: applicationinsights at v2.x and an aliased applicationinsights-v3 (npm:applicationinsights@^3). The dual-version proof runs entirely under mocha (--experimental-test-module-mocks in .mocharc.json node-option enables mock.module() from node:test).
The shared loop pattern in test/src/module-mock-test.js, log-transport-test.js, compose-test.js, and fake-applicationinsights-test.js:
['applicationinsights', 'applicationinsights-v3'].forEach((version) => {
describe(`… ${version}`, () => {
before(async () => {
const ai = await import(version);
mockApplicationinsights(ai); // test/helpers/mock-module.js — see Module-mock helper below
// Cache-bust BOTH compose and FakeApplicationInsights so each iteration
// re-evaluates them under the active mock — without ?v=… the second
// iteration would still hold the first version's TelemetryClient ref.
const bust = `?v=${version}-${++cacheBust}`;
compose = (await import(`../../src/index.js${bust}`)).default;
const { FakeApplicationInsights } = await import(`../../src/fake-applicationinsights.js${bust}`);
// Auto-flush patch: v3's BatchLogRecordProcessor delays exports ~5s and
// explicitly drops maxBatchSize, so wrap each track method to flush after
// the original. v2 flushes are cheap when the channel is idle. The flushes
// are *serialized* through `flushState.chain` (a const holder so ESLint's
// no-loop-func is satisfied) — see the flush-serialization note below.
const flushState = { chain: Promise.resolve() };
for (const m of ['trackTrace', 'trackException', 'trackEvent', 'trackMetric']) {
const original = TelemetryClient.prototype[m];
if (typeof original !== 'function') continue;
mock.method(TelemetryClient.prototype, m, function (...args) {
const r = original.apply(this, args);
if (typeof this.flush === 'function') flushState.chain = flushState.chain.then(() => this.flush()).catch(() => {});
return r;
});
}
});
after(() => mock.restoreAll());
});
});
Module-mock helper (test/helpers/mock-module.js, required for Node >= 24). Every mock.module('applicationinsights', …) call goes through mockApplicationinsights(ai) (and mockModule(url, …) for the example-logger ESM mock) rather than calling mock.module inline. Node 24 deprecated namedExports/defaultExport in favour of a single exports object and rewrote the CJS mock loader (cjsMockModuleLoad) to overlay the named exports onto exports.default with Object.defineProperty. Passing the live ESM namespace ai makes exports.default point at the real applicationinsights module — whose exports are non-configurable getters (compiled TypeScript) — so the overlay throws TypeError: Cannot redefine property: __esModule / Configuration on Node >= 24 (CI's latest). The helper sidesteps it by handing over a fresh, configurable, default-free copy of the named exports (so the loader builds a brand-new exports object), and picks exports on Node >= 24 vs the older namedExports/defaultExport on Node 20/22 (the exports option is silently ignored there). Keep all module mocking funnelled through this helper; don't reintroduce inline mock.module('applicationinsights', { namedExports: ai }).
Flush serialization (@opentelemetry/sdk-logs >= 0.215, pulled in by applicationinsights@3.15). The auto-flush patch must chain its flushes, not fire them concurrently. The new OTel BatchLogRecordProcessorBase._flushAll() guards against concurrent flushes: a forceFlush() that overlaps the previous flush's still-settling async tail (_flushing is only reset after exportCompleted) returns immediately without exporting the just-queued record, which then waits for the ~5s scheduledDelayMillis tick — blowing mocha's 2s timeout. Serializing via flushState.chain.then(() => this.flush()) makes each flush wait for the prior one to fully settle (the SDK's flush() resolves only after _flushAll completes), so nothing races the guard. This is upstream behaviour (opentelemetry-js#6356), surfaced neither in the applicationinsights changelog nor as a flagged breaking change; consumer-facing guidance lives in README → Flush timing (v3). Reliable flushing also means un-awaited telemetry now actually gets delivered — a test that logs without await-ing its expectation can leak that envelope into a later shared fakeAI.expect(n) count collector; drain every log you emit (see compose-test.js#no TelemetryClient config is ok).
- v3-contract divergences kept inside the loop with
if (version === 'applicationinsights')skips:tagOverrides(v3 ignores them — wiretagscome from OTel resource attributes),hasFullStack/v2-shapedparsedStack, bare-instrumentation-key initialisation (v3 requires a full connection string). test/src/applicationinsights-v3-test.js— extra end-to-end coverage of the real v3 SDK driven directly (nomock.module), with explicitawait v3Client.flush(). Its seconddescribeusesmock.timers(setIntervalonly, soflush()and nock keep real timers) to tick the 15-minute metric readers and prove thatenableAutoCollectPerformance: false+enableAutoCollectPreAggregatedMetrics: falsepassed throughapplyClientConfigstop the performance-counter and standard-metric exports; each client gets its own ingestion host so POST counts are unambiguous, andclient.shutdown()inaftertears down the global providers.test/src/statsbeat-test.js— drives v2 statsbeat withmock.timersfromnode:test(tick(900000)fires the 15-minutesetInterval) and asserts the POST to the Microsoft statsbeat resource, then thatapplyClientConfig({ disableStatsbeat: true })stops it. v2 cachesAPPLICATION_INSIGHTS_NO_STATSBEATin theJsonConfigsingleton at first use, so the test flipsJsonConfig.getInstance().disableStatsbeatrather than the env var and restores it inafter.no-warningsin.mocharc.jsonnode-optionsilences the MockTimersExperimentalWarning.
Tests must remain network-free — nock + mocked clients only.
Build & types
- The package is dual ESM/CJS.
package.jsonexportsmapsimport→src/*.jsandrequire→lib/*.cjs. Never hand-editlib/— it is generated by Rollup; editsrc/and rebuild. - Types are JSDoc-driven:
src/*.jscarries@type/@paramannotations,tsconfig.jsonruns withallowJs+checkJs+strict, anddts-buddyemitstypes/index.d.tsfrom those annotations duringnpm run build. Hand-written interfaces live intypes/interfaces.d.tsand are referenced from JSDoc viaimport('../types/interfaces.js'). test/typescript/consumer.tsis a compile-only fixture: real consumer code written against the published.d.tssurface, checked bynpm run tscand never executed. Treat a type error there as a public API regression.
Conventions
- ESLint
eslint.config.jsflat config; key rules:no-console: 2,eqeqeq,prefer-const,require-await, semi always. Prettier: 140 print width, single quotes, 2-space tabs. .gitignoreexcludesCLAUDE.md,.claude,.agents,.pi— AI-assistant artifacts stay local.- The example app reads
example/config/<NODE_ENV>.jsonandexample/.envthroughexp-config@5, which requiresNODE_ENVto be set explicitly and resolves the config directory fromCONFIG_BASE_PATH(default: cwd).test/helpers/setup.jssets both so mocha can run from the repo root;npm start -w exampleruns with cwdexample/, and itsstartscript setsNODE_ENV. applicationinsightspeer dep is>=2 <4. v2 is the primary install; v3 is exercised through the aliasedapplicationinsights-v3devDep. The two surfaces diverge only in the spots called out under v2 / v3 dual support above — keep version-specific knowledge in the helpers listed under v2 / v3 dual support (applyClientConfig,SeverityLevel,parseConnectionString,extractTelemetryItems) rather than scattering version checks through callers.
