diff --git a/.yarn/cache/type-fest-npm-4.3.2-d579a76dbd-8ba1b3d43e.zip b/.yarn/cache/type-fest-npm-4.3.2-d579a76dbd-8ba1b3d43e.zip new file mode 100644 index 00000000..ecd1f54c Binary files /dev/null and b/.yarn/cache/type-fest-npm-4.3.2-d579a76dbd-8ba1b3d43e.zip differ diff --git a/README.md b/README.md index 525a4c51..ca7674cb 100644 --- a/README.md +++ b/README.md @@ -32,5 +32,4 @@ development, not ready for production use, and the feature set is currently lim - Only captures named `function`s and methods. - CommonJS only. - Only whole process recording and Jest per-test recordings. -- No metadata or classmap generation yet. - No exception support. diff --git a/TODO.md b/TODO.md index 9236e74b..c5b23719 100644 --- a/TODO.md +++ b/TODO.md @@ -3,4 +3,3 @@ - configurable output path - configurable traced packages - location paths relative to project root -- test recording \ No newline at end of file diff --git a/package.json b/package.json index 8f69f622..a8a7460e 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "prettier": "^3.0.2", "tmp": "^0.2.1", "ts-node": "^10.9.1", + "type-fest": "^4.3.2", "typescript": "^5.1.6" }, "dependencies": { diff --git a/src/Recording.ts b/src/Recording.ts index fba75a1e..366a18de 100644 --- a/src/Recording.ts +++ b/src/Recording.ts @@ -8,19 +8,26 @@ import { appMapDir } from "./config"; import { makeCallEvent, makeReturnEvent } from "./event"; import type { FunctionInfo } from "./registry"; import { makeClassMap } from "./classMap"; +import { defaultMetadata } from "./metadata"; export default class Recording { - constructor(type: string, ...names: string[]) { - const dirs = [type, ...names]; + constructor(type: AppMap.RecorderType, recorder: string, ...names: string[]) { + const dirs = [recorder, ...names]; const name = dirs.pop()!; // it must have at least one element this.path = join(appMapDir, ...dirs, makeAppMapFilename(name)); this.stream = new AppMapStream(this.path); + this.metadata = { + ...defaultMetadata, + recorder: { type, name: recorder }, + name: names.join(" "), + }; } private nextId = 1; private functionsSeen = new Set(); private stream: AppMapStream | undefined; public readonly path; + public metadata: AppMap.Metadata; functionCall(funInfo: FunctionInfo, thisArg: unknown, args: unknown[]): AppMap.FunctionCallEvent { assert(this.stream); @@ -43,7 +50,10 @@ export default class Recording { } finish(): boolean { - const written = this.stream?.close({ classMap: makeClassMap(this.functionsSeen.keys()) }); + const written = this.stream?.close({ + classMap: makeClassMap(this.functionsSeen.keys()), + metadata: this.metadata, + }); this.stream = undefined; if (written) writtenAppMaps.push(this.path); return !!written; diff --git a/src/bin.ts b/src/bin.ts index d156da7f..35c22be8 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -3,7 +3,7 @@ import { accessSync } from "node:fs"; import { resolve } from "node:path"; import { info } from "./message"; -import { version } from "./version"; +import { version } from "./metadata"; const registerPath = resolve(__dirname, "../dist/register.js"); diff --git a/src/event.ts b/src/event.ts index a6af452a..59adad99 100644 --- a/src/event.ts +++ b/src/event.ts @@ -47,8 +47,8 @@ function resolveParameters(args: unknown[], fun: FunctionInfo): Parameter[] { ); } -function paramName(param: ESTree.Parameter): string | undefined { - switch (param.type) { +function paramName(param: ESTree.Parameter | undefined): string | undefined { + switch (param?.type) { case "Identifier": return param.name; case "AssignmentPattern": diff --git a/src/hooks/jest.ts b/src/hooks/jest.ts index ecc18aa4..fb8e5a1e 100644 --- a/src/hooks/jest.ts +++ b/src/hooks/jest.ts @@ -5,10 +5,11 @@ import type { ESTree } from "meriyah"; import type { Circus } from "@jest/types"; import { expressionFor, wrap } from "."; -import genericTranform from "../transform"; import { call_, identifier } from "../generate"; -import { recording, start } from "../recorder"; import { info } from "../message"; +import Recording from "../Recording"; +import { recording, start } from "../recorder"; +import genericTranform from "../transform"; export function shouldInstrument(url: URL): boolean { return ( @@ -49,11 +50,14 @@ function isId(node: ESTree.Node | null, name: string) { function eventHandler(event: Circus.Event) { switch (event.name) { case "test_fn_start": - start("jest", ...testNames(event.test)); + start(createRecording(event.test)); break; case "test_fn_failure": + recording.metadata.test_status = "failed"; + return recording.finish(); case "test_fn_success": - recording.finish(); + recording.metadata.test_status = "succeeded"; + return recording.finish(); } } @@ -71,3 +75,8 @@ function testNames(test: Circus.TestEntry): string[] { for (let block = test.parent; block.parent; block = block.parent) names.push(block.name); return names.reverse(); } + +function createRecording(test: Circus.TestEntry): Recording { + const recording = new Recording("tests", "jest", ...testNames(test)); + return recording; +} diff --git a/src/metadata.ts b/src/metadata.ts new file mode 100644 index 00000000..7c93ad12 --- /dev/null +++ b/src/metadata.ts @@ -0,0 +1,41 @@ +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import process from "node:process"; + +import type { PackageJson } from "type-fest"; + +import type AppMap from "./AppMap"; +import { root } from "./config"; + +// cannot use import because it's outside src +// eslint-disable-next-line @typescript-eslint/no-var-requires +const pkg = require("../package.json") as PackageJson; + +export const version = pkg.version!; + +export const defaultMetadata: Partial & { client: AppMap.ClientMetadata } = { + client: { + name: pkg.name!, + version, + url: pkg.homepage!, + }, + language: { + name: "ECMAScript", + engine: "Node.js", + version: process.version, + }, +}; + +function readPkgUp(dir: string): PackageJson | undefined { + try { + return JSON.parse(readFileSync(join(dir, "package.json"), "utf-8")) as PackageJson; + } catch { + const parent = dirname(dir); + if (parent === dir) return; + else return readPkgUp(parent); + } +} + +const targetPackage = readPkgUp(root); + +if (targetPackage?.name) defaultMetadata.app = targetPackage.name; diff --git a/src/recorder.ts b/src/recorder.ts index f226eace..220864de 100644 --- a/src/recorder.ts +++ b/src/recorder.ts @@ -5,7 +5,7 @@ import { functions } from "./registry"; import Recording, { writtenAppMaps } from "./Recording"; import commonPathPrefix from "./util/commonPathPrefix"; -export let recording: Recording = new Recording("process", new Date().toISOString()); +export let recording: Recording = new Recording("process", "process", new Date().toISOString()); export function record( this: This, @@ -34,9 +34,9 @@ function isGlobal(obj: unknown): obj is typeof globalThis { return typeof obj === "object" && obj !== null && "global" in obj && obj.global === obj; } -export function start(type: string, ...names: string[]) { +export function start(newRecording: Recording) { assert(!recording.running); - recording = new Recording(type, ...names); + recording = newRecording; } process.on("exit", () => { diff --git a/src/version.ts b/src/version.ts deleted file mode 100644 index 035c93ee..00000000 --- a/src/version.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -const { version: VERSION } = require("../package.json"); - -export const version: string = VERSION; diff --git a/test/__snapshots__/jest.test.ts.snap b/test/__snapshots__/jest.test.ts.snap index 6df54e7f..354e9498 100644 --- a/test/__snapshots__/jest.test.ts.snap +++ b/test/__snapshots__/jest.test.ts.snap @@ -57,6 +57,25 @@ exports[`mapping Jest tests 1`] = ` "thread_id": 0, }, ], + "metadata": { + "app": "jest-appmap-node-test", + "client": { + "name": "appmap-node", + "url": "https://github.com/getappmap/appmap-node", + "version": "0.0.1", + }, + "language": { + "engine": "Node.js", + "name": "ECMAScript", + "version": "test node version", + }, + "name": "sum sums numbers correctly", + "recorder": { + "name": "jest", + "type": "tests", + }, + "test_status": "succeeded", + }, "version": "1.12", }, } diff --git a/test/__snapshots__/simple.test.ts.snap b/test/__snapshots__/simple.test.ts.snap index 761d1d4b..87081d7f 100644 --- a/test/__snapshots__/simple.test.ts.snap +++ b/test/__snapshots__/simple.test.ts.snap @@ -51,6 +51,24 @@ exports[`mapping a simple script 1`] = ` "thread_id": 0, }, ], + "metadata": { + "app": "appmap-node", + "client": { + "name": "appmap-node", + "url": "https://github.com/getappmap/appmap-node", + "version": "0.0.1", + }, + "language": { + "engine": "Node.js", + "name": "ECMAScript", + "version": "test node version", + }, + "name": "test process recording", + "recorder": { + "name": "process", + "type": "process", + }, + }, "version": "1.12", } `; diff --git a/test/helpers.ts b/test/helpers.ts index b614a79f..e9c59f89 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,3 +1,4 @@ +import assert from "node:assert"; import { spawnSync } from "node:child_process"; import { accessSync, readFileSync, rmSync } from "node:fs"; import { resolve } from "node:path"; @@ -5,7 +6,8 @@ import { cwd } from "node:process"; import caller from "caller"; import { globSync } from "fast-glob"; -import assert from "node:assert"; + +import type AppMap from "../src/AppMap"; const binPath = resolve(__dirname, "../bin/appmap-node.js"); @@ -45,6 +47,8 @@ export function readAppmap(path?: string): AppMap { assert(result.events instanceof Array); result.events.forEach(fixEvent); if ("classMap" in result && result.classMap instanceof Array) fixClassMap(result.classMap); + if ("metadata" in result && typeof result.metadata === "object" && result.metadata) + fixMetadata(result.metadata as AppMap.Metadata); return result; } @@ -76,6 +80,11 @@ function fixClassMap(classMap: unknown[]) { } } +function fixMetadata(metadata: AppMap.Metadata) { + if (metadata.recorder.type === "process") metadata.name = "test process recording"; + if (metadata.language) metadata.language.version = "test node version"; +} + function ensureBuilt() { const probePath = resolve(__dirname, "../dist/register.js"); try { @@ -85,4 +94,4 @@ function ensureBuilt() { } } -ensureBuilt(); \ No newline at end of file +ensureBuilt(); diff --git a/yarn.lock b/yarn.lock index 678f8574..0262a33a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1503,6 +1503,7 @@ __metadata: prettier: ^3.0.2 tmp: ^0.2.1 ts-node: ^10.9.1 + type-fest: ^4.3.2 typescript: ^5.1.6 bin: appmap-node: bin/appmap-node.js @@ -4569,6 +4570,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^4.3.2": + version: 4.3.2 + resolution: "type-fest@npm:4.3.2" + checksum: 8ba1b3d43e24888052d8c8859ae9b53124f8200c05808ec9247917ac3441612a7b36bf148a5b14150ef73ce7bad3cdca65ae923d37d2ae466c2b814d369fb975 + languageName: node + linkType: hard + "typescript@npm:^5.1.6": version: 5.1.6 resolution: "typescript@npm:5.1.6"