Skip to content
This repository was archived by the owner on Oct 30, 2025. It is now read-only.

Commit 9d0cfa4

Browse files
authored
Add setupFetchTracing() function (#10)
- Separate `setupHttpTracing` into two functions - `setupHttpTracing` function for incoming requests - `setupFetchTracing` function for outgoing requests - Refactor to separate files new parameters so that we don't need to expose Node.js typings - Add tests for these new functions - Add `node-fetch` devDependency (we use it in tests and examples)
1 parent 8b3a663 commit 9d0cfa4

File tree

11 files changed

+263
-90
lines changed

11 files changed

+263
-90
lines changed

examples/db-example.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async function getDocumentById(ms: number, childOf: SpanContext) {
3434

3535
// example parent function we wish to trace
3636
async function handler(req: IncomingMessage, res: ServerResponse) {
37-
const { spanContext } = setupHttpTracing({ tracer, req, res });
37+
const spanContext = setupHttpTracing({ tracer, req, res });
3838
console.log(spanContext.toTraceId(), spanContext.toSpanId());
3939
let statusCode: number;
4040
let data: any;

examples/routing-example.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import {
44
SpanContext,
55
DeterministicSampler,
66
setupHttpTracing,
7+
setupFetchTracing,
78
} from '../src/index';
89

10+
import nodeFetch from 'node-fetch';
11+
912
const tracer = new Tracer(
1013
{
1114
serviceName: 'routing-example',
@@ -56,7 +59,8 @@ async function route(path: string, childOf: SpanContext) {
5659

5760
// example parent function we wish to trace
5861
async function handler(req: IncomingMessage, res: ServerResponse) {
59-
const { spanContext, fetch } = setupHttpTracing({ tracer, req, res });
62+
const spanContext = setupHttpTracing({ tracer, req, res });
63+
const fetch = setupFetchTracing({ spanContext, fetch: nodeFetch });
6064
console.log(spanContext.toTraceId(), spanContext.toSpanId());
6165
let statusCode = 200;
6266

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@types/node": "^11.9.4",
2121
"@types/node-fetch": "^2.1.6",
2222
"@types/tape": "^4.2.33",
23+
"node-fetch": "^2.3.0",
2324
"prettier": "^1.16.4",
2425
"tape": "^4.10.1",
2526
"typescript": "^3.3.3"

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as Tags from './tags';
44
import { DeterministicSampler } from './deterministic-sampler';
55
import { SamplerBase } from './shared';
66
import { setupHttpTracing } from './setup-http';
7+
import { setupFetchTracing } from './setup-fetch';
78

89
export {
910
Tracer,
@@ -12,4 +13,5 @@ export {
1213
DeterministicSampler,
1314
SamplerBase,
1415
setupHttpTracing,
16+
setupFetchTracing,
1517
};

src/setup-fetch.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { SpanContext } from './span-context';
2+
import * as Tags from './tags';
3+
import * as Hdrs from './headers';
4+
import { Request, RequestInit, Headers } from 'node-fetch';
5+
type Fetch = (url: string | Request, opts?: RequestInit) => void;
6+
7+
interface SetupFetchTracingOptions<T> {
8+
spanContext: SpanContext;
9+
fetch?: T;
10+
}
11+
12+
export function setupFetchTracing<T>(options: SetupFetchTracingOptions<T>): T {
13+
const { fetch, spanContext } = options;
14+
let fetchOriginal: Fetch;
15+
if (fetch) {
16+
fetchOriginal = (fetch as unknown) as Fetch;
17+
} else {
18+
fetchOriginal = require('node-fetch');
19+
}
20+
21+
const fetchTracing = (url: string | Request, opts?: RequestInit) => {
22+
if (!opts) {
23+
opts = { headers: new Headers() };
24+
}
25+
const headers = new Headers(opts.headers as any);
26+
opts.headers = headers;
27+
28+
const traceId = spanContext.toTraceId();
29+
const parentId = spanContext.toSpanId();
30+
const priority = spanContext.getTag(Tags.SAMPLING_PRIORITY);
31+
32+
headers.set(Hdrs.TRACE_ID, traceId);
33+
if (typeof parentId !== 'undefined') {
34+
headers.set(Hdrs.PARENT_ID, parentId);
35+
}
36+
if (typeof priority !== 'undefined') {
37+
headers.set(Hdrs.PRIORITY, priority);
38+
}
39+
40+
return fetchOriginal(url, opts);
41+
};
42+
43+
// TS doesn't know about decorated runtime data
44+
// so we copy from the original just to be safe.
45+
for (const key of Object.keys(fetchOriginal)) {
46+
const tracing = fetchTracing as any;
47+
const original = fetchOriginal as any;
48+
tracing[key] = original[key];
49+
}
50+
51+
fetchTracing.default = fetchTracing;
52+
53+
return (fetchTracing as unknown) as T;
54+
}

src/setup-http.ts

Lines changed: 8 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
1-
import { IncomingMessage, ServerResponse } from 'http';
21
import { SpanContext } from './span-context';
32
import * as Tags from './tags';
43
import * as Hdrs from './headers';
54
import { Tracer } from './tracer';
6-
import { SpanOptions, SpanTags } from './shared';
7-
import FetchTemp, { Request, RequestInit, Headers } from 'node-fetch';
8-
type Fetch = typeof FetchTemp;
5+
import { SpanOptions, SpanTags, HttpRequest, HttpResponse } from './shared';
96

107
interface SetupHttpTracingOptions {
118
name?: string;
129
tracer: Tracer;
13-
req: IncomingMessage;
14-
res: ServerResponse;
15-
fetch?: Fetch;
10+
req: HttpRequest;
11+
res: HttpResponse;
1612
}
1713

1814
export function setupHttpTracing(options: SetupHttpTracingOptions) {
19-
const { name = 'setupHttpTracing', tracer, req, res, fetch } = options;
15+
const { name = 'setupHttpTracing', tracer, req, res } = options;
2016
const spanOptions = getSpanOptions(req);
2117
const span = tracer.startSpan(name, spanOptions);
2218
const spanContext = span.context();
@@ -30,17 +26,15 @@ export function setupHttpTracing(options: SetupHttpTracingOptions) {
3026
span.finish();
3127
});
3228

33-
const fetchTracing = setupFetch(fetch, spanContext);
34-
35-
return { spanContext, fetch: fetchTracing };
29+
return spanContext;
3630
}
3731

38-
function getFirstHeader(req: IncomingMessage, key: string) {
32+
function getFirstHeader(req: HttpRequest, key: string): string | undefined {
3933
const value = req.headers[key];
4034
return Array.isArray(value) ? value[0] : value;
4135
}
4236

43-
function getSpanOptions(req: IncomingMessage) {
37+
function getSpanOptions(req: HttpRequest): SpanOptions {
4438
const tags: SpanTags = {};
4539
tags[Tags.HTTP_METHOD] = req.method;
4640
tags[Tags.HTTP_URL] = req.url;
@@ -57,52 +51,5 @@ function getSpanOptions(req: IncomingMessage) {
5751
childOf = new SpanContext(traceId, parentId, tags);
5852
}
5953

60-
const options: SpanOptions = { tags, childOf };
61-
return options;
62-
}
63-
64-
function setupFetch(fetch: Fetch | undefined, spanContext: SpanContext) {
65-
let fetchOriginal: Fetch;
66-
if (fetch) {
67-
fetchOriginal = fetch;
68-
} else {
69-
fetchOriginal = require('node-fetch');
70-
}
71-
72-
function fetchTracing(url: string | Request, opts?: RequestInit) {
73-
if (!opts) {
74-
opts = { headers: new Headers() };
75-
}
76-
const headers =
77-
opts.headers instanceof Headers
78-
? opts.headers
79-
: new Headers(opts.headers as any);
80-
81-
const traceId = spanContext.toTraceId();
82-
const parentId = spanContext.toSpanId();
83-
const priority = spanContext.getTag(Tags.SAMPLING_PRIORITY);
84-
85-
headers.set(Hdrs.TRACE_ID, traceId);
86-
if (typeof parentId !== 'undefined') {
87-
headers.set(Hdrs.PARENT_ID, parentId);
88-
}
89-
if (typeof priority !== 'undefined') {
90-
headers.set(Hdrs.PRIORITY, priority);
91-
}
92-
93-
return fetchOriginal(url, opts);
94-
}
95-
96-
fetchTracing.default = fetchTracing;
97-
fetchTracing.isRedirect = fetchOriginal.isRedirect;
98-
99-
// TS doesn't know about decorated runtime data
100-
// so we copy from the original just to be safe.
101-
for (const key of Object.keys(fetchOriginal)) {
102-
const tracing = fetchTracing as any;
103-
const original = fetchOriginal as any;
104-
tracing[key] = original[key];
105-
}
106-
107-
return fetchTracing;
54+
return { tags, childOf };
10855
}

src/shared.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,14 @@ export interface SpanOptions {
2323
export interface SamplerBase {
2424
sample(data: string): boolean;
2525
}
26+
27+
export interface HttpRequest {
28+
headers: { [header: string]: string | string[] | undefined };
29+
method?: string;
30+
url?: string;
31+
}
32+
33+
export interface HttpResponse {
34+
statusCode: number;
35+
on(event: string, listener: () => void): this;
36+
}

test/dummy-honey.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Libhoney, { HoneyEvent, HoneyOptions } from 'libhoney';
2+
3+
const noop = () => {};
4+
5+
interface DummyOptions {
6+
addField?: (key: string, value: any) => void;
7+
send?: () => void;
8+
}
9+
10+
class DummyHoney extends Libhoney {
11+
private dummyOptions: DummyOptions;
12+
13+
constructor(options: HoneyOptions, dummyOptions?: DummyOptions) {
14+
super(options);
15+
this.dummyOptions = dummyOptions || {};
16+
}
17+
18+
newEvent(): HoneyEvent {
19+
const { addField, send } = this.dummyOptions;
20+
return {
21+
addField: addField || noop,
22+
send: send || noop,
23+
};
24+
}
25+
}
26+
27+
export function newDummyHoney(options?: DummyOptions) {
28+
return new DummyHoney(
29+
{
30+
writeKey: 'test',
31+
dataset: 'test',
32+
disabled: true,
33+
},
34+
options,
35+
);
36+
}

test/setup-fetch.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import test from 'tape';
2+
import { setupFetchTracing } from '../src/setup-fetch';
3+
import { SpanContext } from '../src/span-context';
4+
import * as Tags from '../src/tags';
5+
import * as Hdrs from '../src/headers';
6+
import { RequestInit, Headers } from 'node-fetch';
7+
8+
test('setup-fetch test empty fetch', t => {
9+
t.plan(1);
10+
const spanContext = new SpanContext('trace-id', 'span-id', {});
11+
const fetch = setupFetchTracing({ spanContext });
12+
t.equal(typeof fetch, 'function');
13+
});
14+
15+
test('setup-fetch test fetch properties', t => {
16+
t.plan(1);
17+
const spanContext = new SpanContext('trace-id', 'span-id', {});
18+
const oldFetch = (_: string) => {};
19+
oldFetch.dummy = true;
20+
const newFetch = setupFetchTracing({ spanContext, fetch: oldFetch });
21+
t.equal(newFetch.dummy, oldFetch.dummy);
22+
});
23+
24+
test('setup-fetch test fetch response', async t => {
25+
t.plan(1);
26+
const spanContext = new SpanContext('trace-id', 'span-id', {});
27+
const oldFetch = (_: string) => {
28+
return 'hello';
29+
};
30+
const newFetch = setupFetchTracing({ spanContext, fetch: oldFetch });
31+
const oldResponse = await oldFetch('fake-url');
32+
const newResponse = await newFetch('fake-url');
33+
t.equal(newResponse, oldResponse);
34+
});
35+
36+
test('setup-fetch test fetch headers when empty', async t => {
37+
t.plan(3);
38+
const spanContext = new SpanContext('trace-id', 'span-id', {
39+
[Tags.SAMPLING_PRIORITY]: 99,
40+
});
41+
const oldFetch = (_: string, opts?: RequestInit) => {
42+
if (opts && opts.headers) {
43+
const headers =
44+
opts.headers instanceof Headers
45+
? opts.headers
46+
: new Headers(opts.headers as any);
47+
t.equal(headers.get(Hdrs.TRACE_ID), 'trace-id');
48+
t.equal(headers.get(Hdrs.PARENT_ID), 'span-id');
49+
t.equal(headers.get(Hdrs.PRIORITY), '99');
50+
}
51+
return 'hello';
52+
};
53+
const newFetch = setupFetchTracing({ spanContext, fetch: oldFetch });
54+
await newFetch('fake-url');
55+
});
56+
57+
test('setup-fetch test fetch headers when non-empty', async t => {
58+
t.plan(4);
59+
const spanContext = new SpanContext('trace-id', 'span-id', {
60+
[Tags.SAMPLING_PRIORITY]: 99,
61+
});
62+
const oldFetch = (_: string, opts?: RequestInit) => {
63+
if (opts && opts.headers) {
64+
const headers = new Headers(opts.headers as any);
65+
t.equal(headers.get(Hdrs.TRACE_ID), 'trace-id');
66+
t.equal(headers.get(Hdrs.PARENT_ID), 'span-id');
67+
t.equal(headers.get(Hdrs.PRIORITY), '99');
68+
t.equal(headers.get('x-test-header'), 'the-value');
69+
}
70+
return 'hello';
71+
};
72+
const newFetch = setupFetchTracing({ spanContext, fetch: oldFetch });
73+
await newFetch('fake-url', { headers: { 'x-test-header': 'the-value' } });
74+
});

0 commit comments

Comments
 (0)