Skip to content

Commit

Permalink
Instrument Node 17+ internal Fetch
Browse files Browse the repository at this point in the history
  • Loading branch information
carbonrobot committed Sep 28, 2023
1 parent 917d5c5 commit d614ced
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 67 deletions.
3 changes: 3 additions & 0 deletions examples/next/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

import { enableTracing } from '@envyjs/nextjs';
enableTracing({ serviceName: 'example-nextjs', ignoreRSC: true, filter: request => request.host !== 'dog.ceo' });

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
Expand Down
3 changes: 3 additions & 0 deletions examples/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@
"react-dom": "18.2.0",
"tailwindcss": "3.3.3",
"typescript": "5.2.2"
},
"devDependencies": {
"@envyjs/nextjs": "*"
}
}
63 changes: 63 additions & 0 deletions packages/core/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Event } from './event';
import { HttpRequest } from './http';

// TODO: the types in this file are from lib/dom
// we need to replace them with a platform agnostic version

function formatFetchHeaders(headers: HeadersInit | Headers | undefined): HttpRequest['requestHeaders'] {
if (headers) {
if (Array.isArray(headers)) {
return headers.reduce<HttpRequest['requestHeaders']>((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
} else if (headers instanceof Headers) {
return Object.fromEntries(headers.entries());
} else {
return headers;
}
}

return {};
}

export function fetchRequestToEvent(id: string, input: RequestInfo | URL, init?: RequestInit): Event {
let url: URL;
if (typeof input === 'string') {
url = new URL(input);
} else if (input instanceof Request) {
url = new URL(input.url);
} else {
url = input;
}

return {
id,
parentId: undefined,
timestamp: Date.now(),
http: {
method: (init?.method ?? 'GET') as HttpRequest['method'],
host: url.host,
port: parseInt(url.port, 10),
path: url.pathname,
url: url.toString(),
requestHeaders: formatFetchHeaders(init?.headers),
requestBody: init?.body?.toString() ?? undefined,
},
};
}

export async function fetchResponseToEvent(req: Event, response: Response): Promise<Event> {
return {
...req,

http: {
...req.http!,
httpVersion: response.type,
statusCode: response.status,
statusMessage: response.statusText,
responseHeaders: formatFetchHeaders(response.headers),
responseBody: await response.text(),
},
};
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './consts';
export * from './event';
export * from './fetch';
export * from './graphql';
export * from './http';
export * from './middleware';
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
import { HttpRequest } from '.';

export type FilterableHttpRequest = Pick<HttpRequest, 'host' | 'method'>;

export interface Options {
/**
* A unique identifier for the application
*/
serviceName: string;

/**
* Set to true to enable debugging of exported messages
* @default false
*/
debug?: boolean;

/**
* Define a function to filter http requests
* @default undefined
*/
filter?: (request: FilterableHttpRequest) => boolean;
}
25 changes: 23 additions & 2 deletions packages/nextjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,38 @@ import { TracingOptions, enableTracing as nodeTracing } from '@envyjs/node';

import { Routes } from './route';

export type NextjsTracingOptions = TracingOptions;
export type NextjsTracingOptions = TracingOptions & {
/**
* Set to true to ignore browser calls to React Server Components
*/
ignoreRSC?: boolean;
};

// nextjs dev mode can run this multiple times
// prevent multiple registrations with a flag
let initialized = false;

export function enableTracing(options: NextjsTracingOptions) {
const nextjsOptions: NextjsTracingOptions = {
...options,
};

if (options.ignoreRSC === true) {
nextjsOptions.filter = request => {
if (request.host?.includes('127.0.0.1:6') || request.host?.includes('localhost:6')) return false;

if (options.filter) {
return options.filter(request);
}

return true;
};
}

if (!initialized) {
initialized = true;
return nodeTracing({
...options,
...nextjsOptions,
plugins: [...(options.plugins || []), Routes],
});
}
Expand Down
4 changes: 4 additions & 0 deletions packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,9 @@
"@types/ws": "^8.5.5",
"node-fetch": "^2.7.0",
"ts-node": "^10.9.1"
},
"optionalDependencies": {
"bufferutil": "4.0.7",
"utf-8-validate": "6.0.3"
}
}
27 changes: 27 additions & 0 deletions packages/node/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Plugin, fetchRequestToEvent, fetchResponseToEvent } from '@envyjs/core';

import { generateId } from './id';

export const Fetch: Plugin = (_options, exporter) => {
const { fetch: originalFetch } = global;
global.fetch = async (...args) => {
const id = generateId();
const startTs = performance.now();

// export the initial request data
const reqEvent = fetchRequestToEvent(id, ...args);
exporter.send(reqEvent);

// execute the actual request
const response = await originalFetch(...args);
const responseClone = response.clone();
const resEvent = await fetchResponseToEvent(reqEvent, responseClone);

resEvent.http!.duration = performance.now() - startTs;

// export the final request data which now includes response
exporter.send(resEvent);

return response;
};
};
10 changes: 8 additions & 2 deletions packages/node/src/tracing.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Exporter, Meta, Middleware, Options, Plugin, Sanity } from '@envyjs/core';

import { WebSocketClient } from './client';
import { Fetch } from './fetch';
import { Http } from './http';
import log from './log';

Expand All @@ -24,10 +25,15 @@ export function enableTracing(options: TracingOptions) {
const exporter: Exporter = {
send(message) {
const result = middleware.reduce((prev, t) => t(prev, options), message);
wsClient.send(result);
if (result) {
if (result.http && options.filter && options.filter(result.http) === false) {
return;
}
wsClient.send(result);
}
},
};

// initialize all plugins
[Http, ...(options.plugins || [])].forEach(fn => fn(options, exporter));
[Http, Fetch, ...(options.plugins || [])].forEach(fn => fn(options, exporter));
}
65 changes: 4 additions & 61 deletions packages/web/src/http.ts → packages/web/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Event, HttpRequest, Plugin } from '@envyjs/core';
import { Plugin, fetchRequestToEvent, fetchResponseToEvent } from '@envyjs/core';

import { generateId } from './id';
import { calculateTiming } from './performance';

export const Http: Plugin = (_options, exporter) => {
export const Fetch: Plugin = (_options, exporter) => {
const { fetch: originalFetch } = window;
window.fetch = async (...args) => {
const id = generateId();
const startTs = performance.now();

// export the initial request data
const reqEvent = fetchRequestToEvent(...args);
const reqEvent = fetchRequestToEvent(id, ...args);
exporter.send(reqEvent);

performance.mark(reqEvent.id, { detail: { type: 'start' } });
Expand Down Expand Up @@ -55,61 +56,3 @@ export const Http: Plugin = (_options, exporter) => {
return response;
};
};

function formatHeaders(headers: HeadersInit | Headers | undefined): HttpRequest['requestHeaders'] {
if (headers) {
if (Array.isArray(headers)) {
return headers.reduce<HttpRequest['requestHeaders']>((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
} else if (headers instanceof Headers) {
return Object.fromEntries(headers.entries());
} else {
return headers;
}
}

return {};
}

function fetchRequestToEvent(input: RequestInfo | URL, init?: RequestInit): Event {
let url: URL;
if (typeof input === 'string') {
url = new URL(input);
} else if (input instanceof Request) {
url = new URL(input.url);
} else {
url = input;
}

return {
id: generateId(),
parentId: undefined,
timestamp: Date.now(),
http: {
method: (init?.method ?? 'GET') as HttpRequest['method'],
host: url.host,
port: parseInt(url.port, 10),
path: url.pathname,
url: url.toString(),
requestHeaders: formatHeaders(init?.headers),
requestBody: init?.body?.toString() ?? undefined,
},
};
}

async function fetchResponseToEvent(req: Event, response: Response): Promise<Event> {
return {
...req,

http: {
...req.http!,
httpVersion: response.type,
statusCode: response.status,
statusMessage: response.statusText,
responseHeaders: formatHeaders(response.headers),
responseBody: await response.text(),
},
};
}
4 changes: 2 additions & 2 deletions packages/web/src/tracing.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DEFAULT_WEB_SOCKET_PORT, Exporter, Meta, Middleware, Plugin, Sanity } from '@envyjs/core';

import { WebSocketClient } from './client';
import { Http } from './http';
import { Fetch } from './fetch';
import log from './log';
import { Options } from './options';

Expand Down Expand Up @@ -36,7 +36,7 @@ export async function enableTracing(options: TracingOptions): Promise<void> {
};

// initialize all plugins
[Http, ...(options.plugins || [])].forEach(fn => fn(options, exporter));
[Fetch, ...(options.plugins || [])].forEach(fn => fn(options, exporter));

resolve();
});
Expand Down
19 changes: 19 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3465,6 +3465,13 @@ buffer@^5.5.0||^6.0.0:
base64-js "^1.3.1"
ieee754 "^1.2.1"

[email protected]:
version "4.0.7"
resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad"
integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==
dependencies:
node-gyp-build "^4.3.0"

bundle-name@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-3.0.0.tgz#ba59bcc9ac785fb67ccdbf104a2bf60c099f0e1a"
Expand Down Expand Up @@ -7204,6 +7211,11 @@ [email protected]:
resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz#5d2632bbde0ab2f6e22f1bbac2199b07244ae0b3"
integrity sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==

node-gyp-build@^4.3.0:
version "4.6.1"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e"
integrity sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==

node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
Expand Down Expand Up @@ -9299,6 +9311,13 @@ use-latest@^1.2.1:
dependencies:
use-isomorphic-layout-effect "^1.1.1"

[email protected]:
version "6.0.3"
resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.3.tgz#7d8c936d854e86b24d1d655f138ee27d2636d777"
integrity sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==
dependencies:
node-gyp-build "^4.3.0"

util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
Expand Down

0 comments on commit d614ced

Please sign in to comment.