Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
dividedmind committed Jan 27, 2025
1 parent 5fde6c1 commit fb19573
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 22 deletions.
6 changes: 5 additions & 1 deletion src/hooks/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ import {
} from "../recorder";
import { getTime } from "../util/getTime";
import { readFile, rm } from "node:fs/promises";
import { debuglog } from "node:util";

type HTTP = typeof http | typeof https;

// keep track of createServer proxies to avoid double-wrapping
const proxies = new WeakSet<object>();

const debug = debuglog("appmap:http");

export default function httpHook(mod: HTTP) {
if (!proxies.has(mod.createServer)) {
const proxy = new Proxy(mod.createServer, {
Expand Down Expand Up @@ -292,13 +295,14 @@ function handleRequest(request: http.IncomingMessage, response: http.ServerRespo
if (!isActive(recording)) return;
if (fixupEvent(request, requestEvents[idx])) recording.fixup(requestEvents[idx]);

recording.httpResponse(
const event = recording.httpResponse(
requestEvents[idx].id,
elapsed,
response.statusCode,
headers,
returnValue,
);
debug("http response: %o", event);
});

// If there is a test or remote recording we don't create a separate request recording
Expand Down
15 changes: 12 additions & 3 deletions src/hooks/instrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function transform(
if (sourceMap?.originalSources.length) {
if (!originalSourceShouldBeInstrumented.has(location.path)) {
const url = pathToFileURL(location.path);
const originalSource = sourceMap.originalSources.find((s) => s.href == url.href);
const originalSource = url;
originalSourceShouldBeInstrumented.set(
location.path,
originalSource != undefined && shouldInstrument(originalSource),
Expand Down Expand Up @@ -103,13 +103,17 @@ export function transform(
if (!methodHasName(method)) return;
// Can't record generator methods because of potential super keyword inside.
if (method.value.generator) return;
// debug("ancestors: %o", ancestors);
const klass = findLast(ancestors, isNamedClass);
debug(`MethodDefinition: ${method.kind}, ${klass?.id.name}.${method.key.name}`);
if (!klass) return;

const { name } = method.key;
const qname = [klass.id.name, name].join(".");
debug(`qname: ${qname}`);

const location = locate(method);
debug(`location: ${JSON.stringify(location)}`);
if (shouldSkipFunction(location, name, qname)) return;
assert(location);

Expand Down Expand Up @@ -228,7 +232,9 @@ function makeLocator(
return ({ loc }: ESTree.Node) => {
if (!loc?.source) return undefined;
const mapped = sourceMap.originalPositionFor(loc.start);
if (mapped?.line) return { path: mapped.source, lineno: mapped.line };
const url = new URL(mapped.source, "file:///");
if (url.protocol === "file:" && mapped?.line)
return { path: url.pathname, lineno: mapped.line };
};
else
return ({ loc }: ESTree.Node) =>
Expand Down Expand Up @@ -319,6 +325,7 @@ function shouldInstrument_(url: URL) {
if (url.pathname.endsWith(".json")) return false;

const filePath = fileURLToPath(url);
debug("shouldInstrument: %s", filePath);
return !!config().packages.match(filePath);
}

Expand All @@ -327,10 +334,12 @@ function shouldInstrument_(url: URL) {
// instrument.transform(), but within it, we should check each function to determine
// if its original source URL needs to be instrumented.
export function shouldInstrument(url: URL, sourceMap?: CustomSourceMapConsumer): boolean {
debug("shouldInstrument: %s", url);
if (sourceMap?.originalSources.length && shouldMatchOriginalSourcePaths(url))
return sourceMap.originalSources.some((s) => shouldInstrument_(s));
return sourceMap.originalSources.some((s) => shouldInstrument_(s)) || shouldInstrument_(url);

const result = shouldInstrument_(url);
debug("shouldInstrument: %s -> %s", url, result);
return result;
}

Expand Down
7 changes: 7 additions & 0 deletions src/hooks/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type sqlite from "sqlite3";

import { getActiveRecordings, isActive } from "../recorder";
import { getTime } from "../util/getTime";
import { debuglog } from "node:util";

type RecordingProxyTarget =
| typeof sqlite.Database.prototype.exec
Expand All @@ -11,6 +12,8 @@ type RecordingProxyTarget =
| typeof sqlite.Statement.prototype.get
| typeof sqlite.Statement.prototype.each;

const debug = debuglog("appmap:sqlite");

export default function sqliteHook(mod: typeof sqlite) {
mod.Statement.prototype.run = createRecordingProxy(mod.Statement.prototype.run);
mod.Statement.prototype.all = createRecordingProxy(mod.Statement.prototype.all);
Expand Down Expand Up @@ -38,6 +41,7 @@ function createRecordingProxy<T extends RecordingProxyTarget>(
) {
return new Proxy(proxyTarget, {
apply(target, thisArg, argArray: Parameters<typeof proxyTarget>) {
debug("%s %s", target.name, thisArg.sql);

Check failure on line 44 in src/hooks/sqlite.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe member access .sql on an `any` value
// Extract sql. If thisArg is a Statement then it has a sql property.
// Otherwise thisArg is a Database and the sql must be the first element of the argArray.
let sql: string;
Expand All @@ -51,8 +55,11 @@ function createRecordingProxy<T extends RecordingProxyTarget>(
sql = argArray[0];
}

debug("sql: %s", sql);

const recordings = getActiveRecordings();
const callEvents = recordings.map((recording) => recording.sqlQuery("sqlite", sql));
debug("callEvents: %o", callEvents);
const startTime = getTime();

// Extract callback argument(s) to functionArgs
Expand Down
4 changes: 1 addition & 3 deletions src/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,7 @@ export function record<This, Return>(

const startTime = getTime();
try {
const result = funInfo.async
? Recording.fork(() => fun.apply(this, args))
: fun.apply(this, args);
const result = Recording.fork(() => fun.apply(this, args));
recordings.forEach((recording, idx) =>
recording.functionReturn(callEvents[idx].id, result, startTime),
);
Expand Down
20 changes: 5 additions & 15 deletions src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default function transform(code: string, url: URL, hooks = defaultHooks):

const hook = findHook(url, sourceMap, hooks);
if (!hook) return code;
console.debug("Using hook %s for %s", hook.constructor.name, url);

Check failure on line 68 in src/transform.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement

if (hooks.some((h) => h.shouldIgnore?.(url))) return code;

Expand Down Expand Up @@ -112,8 +113,9 @@ function dumpTree(xformed: ESTree.Program, url: URL) {

export class CustomSourceMapConsumer extends SourceMapConsumer {
public originalSources: URL[];
constructor(rawSourceMap: RawSourceMap) {
super(rawSourceMap);
constructor(rawSourceMap: RawSourceMap, url?: string) {
super(rawSourceMap, url);

Check failure on line 117 in src/transform.ts

View workflow job for this annotation

GitHub Actions / windows-test

Expected 1 arguments, but got 2.
debug("source map: %s", url);
this.originalSources = rawSourceMap.sources.map((s) => pathToFileURL(s));
}
}
Expand All @@ -138,19 +140,7 @@ export function getSourceMap(fileUrl: URL, code: string): CustomSourceMapConsume
throw new Error(`Unsupported source map protocol: ${sourceMapUrl.protocol}`);
}

sourceMap.sources = sourceMap.sources.map((source) => {
const rootedSource = (sourceMap.sourceRoot ?? "") + source;
// On Windows, we get incorrect result with URL if the original path contains spaces.
// source: 'C:/Users/John Doe/projects/appmap-node/test/typescript/index.ts'
// => result: 'C:/Users/John%20Doe/projects/appmap-node/test/typescript/index.ts'
// This check prevents it.
if (rootedSource.match(/^[a-zA-Z]:[/\\]/)) return rootedSource;

const url = new URL(rootedSource, fileUrl);
return url.protocol === "file:" ? fileURLToPath(url) : url.toString();
});

return new CustomSourceMapConsumer(sourceMap);
return new CustomSourceMapConsumer(sourceMap, sourceMapUrl.toString());
}

function parseDataUrl(fileUrl: URL) {
Expand Down
1 change: 1 addition & 0 deletions test/sourceMapPath/built/index.js.map

Large diffs are not rendered by default.

104 changes: 104 additions & 0 deletions types/source-map-js.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
export interface StartOfSourceMap {
file?: string;
sourceRoot?: string;
}

export interface RawSourceMap extends StartOfSourceMap {
version: string;
sources: string[];
names: string[];
sourcesContent?: string[];
mappings: string;
}

export interface Position {
line: number;
column: number;
}

export interface LineRange extends Position {
lastColumn: number;
}

export interface FindPosition extends Position {
// SourceMapConsumer.GREATEST_LOWER_BOUND or SourceMapConsumer.LEAST_UPPER_BOUND
bias?: number;
}

export interface SourceFindPosition extends FindPosition {
source: string;
}

export interface MappedPosition extends Position {
source: string;
name?: string;
}

export interface MappingItem {
source: string | null;
generatedLine: number;
generatedColumn: number;
originalLine: number | null;
originalColumn: number | null;
name: string | null;
}

export class SourceMapConsumer {
static GENERATED_ORDER: number;
static ORIGINAL_ORDER: number;

static GREATEST_LOWER_BOUND: number;
static LEAST_UPPER_BOUND: number;

constructor(rawSourceMap: RawSourceMap);
readonly file: string | undefined | null;
readonly sourceRoot: string | undefined | null;
readonly sourcesContent: readonly string[] | null | undefined;
readonly sources: readonly string[]

computeColumnSpans(): void;
originalPositionFor(generatedPosition: FindPosition): MappedPosition;
generatedPositionFor(originalPosition: SourceFindPosition): LineRange;
allGeneratedPositionsFor(originalPosition: MappedPosition): Position[];
hasContentsOfAllSources(): boolean;
sourceContentFor(source: string, returnNullOnMissing?: boolean): string | null;
eachMapping(callback: (mapping: MappingItem) => void, context?: any, order?: number): void;
}

export interface Mapping {
generated: Position;
original?: Position | null;
source?: string | null;
name?: string | null;
}

export class SourceMapGenerator {
constructor(startOfSourceMap?: StartOfSourceMap);
static fromSourceMap(sourceMapConsumer: SourceMapConsumer, startOfSourceMap?: StartOfSourceMap): SourceMapGenerator;
addMapping(mapping: Mapping): void;
setSourceContent(sourceFile: string, sourceContent: string | null | undefined): void;
applySourceMap(sourceMapConsumer: SourceMapConsumer, sourceFile?: string, sourceMapPath?: string): void;
toString(): string;
toJSON(): RawSourceMap;
}

export interface CodeWithSourceMap {
code: string;
map: SourceMapGenerator;
}

export class SourceNode {
constructor();
constructor(line: number, column: number, source: string);
constructor(line: number, column: number, source: string, chunk?: string, name?: string);
static fromStringWithSourceMap(code: string, sourceMapConsumer: SourceMapConsumer, relativePath?: string): SourceNode;
add(chunk: string): void;
prepend(chunk: string): void;
setSourceContent(sourceFile: string, sourceContent: string): void;
walk(fn: (chunk: string, mapping: MappedPosition) => void): void;
walkSourceContents(fn: (file: string, content: string) => void): void;
join(sep: string): SourceNode;
replaceRight(pattern: string, replacement: string): SourceNode;
toString(): string;
toStringWithSourceMap(startOfSourceMap?: StartOfSourceMap): CodeWithSourceMap;
}

0 comments on commit fb19573

Please sign in to comment.