Repository: honeycombio/beeline-nodejs
Pull request #116 (2019-01-23 03:52:05)

custom context should be sent to async spans as well
Async spans are implemented essentially as separate traces with the async span as the root span (though they have the same trace id as their parent, and the parent_id is set correctly on this root span.)

We were initializing the `customContext` map the same way the root trace does, though, which means it was a new empty object, `{}`.  This means `customContext` isn't shared correctly, and the async spans won't have those fields added.

Instead of initializing it to a new object, just share (by ref) the same object across all async spans and their parent trace. 


Patch:
diff --git a/docs/API.md b/docs/API.md
index 6b0b8c9..b9204cf 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -1,9 +1,7 @@
-# Public API
+**This document is intended for anyone looking to write custom instrumentation for your favorite NPM package.**
 
-The beeline does a lot of magic for you, but there will probably be parts of your application where you'll want to add
-some additional instrumentation. There might also be problems with async context propagation that you'll need to work
-around while waiting for a bug fix. We now have a public API for you to use that addresses both those use-cases. It'll
-also help if you want to write a custom instrumentation for your favorite npm package :)
+If you're looking to understand how to **use** the Beeline for Node.js in your application,
+please check out our [Usage and Examples](https://docs.honeycomb.io/beeline/nodejs/) documentation instead.
 
 ## Accessing the API
 
@@ -224,7 +222,7 @@ If you're dealing with multiple services (either on the same host or different o
 #### marshalTraceContext()
 
 ```javascript
-beeline.marshalTraceContext();
+beeline.marshalTraceContext(beeline.getTraceContext());
 ```
 
 Returns a serialized form of the current trace context (including the trace id and the current span), encoded as a string. The format is documented at https://github.com/honeycombio/beeline-nodejs/blob/master/lib/propagation.js#L16
@@ -232,7 +230,7 @@ Returns a serialized form of the current trace context (including the trace id a
 example:
 
 ```javascript
-let traceContext = beeline.marshalTraceContext();
+let traceContext = beeline.marshalTraceContext(beeline.getTraceContext());
 console.log(traceContext); // => 1;trace_id=weofijwoeifj,parent_id=owefjoweifj,context=SGVsbG8gV29ybGQ=
 ```
 
@@ -266,7 +264,7 @@ service1:
   // this next line isn't necessary if you're within an express handler
   let trace = beeline.startTrace();
 
-  let traceContext = beeline.marshalTraceContext();
+  let traceContext = beeline.marshalTraceContext(beeline.getTraceContext);
   try {
     await service2Client.doSomething({
       // add the traceContext in our RPC call payload
@@ -435,7 +433,7 @@ Forces the function `fn` to be invoked with the trace context active when this c
 
 example:
 
-````javascript
+```javascript
 myDBLibrary.query("select * from table",
                   beeline.bindFunctionToTrace(rows => {
                     // inside this function the trace is guaranteed to be
@@ -449,7 +447,7 @@ myDBLibrary.query("select * from table",
 
 ```javascript
 beeline.runWithoutTrace(fn)
-````
+```
 
 Immediately executes fn _outside_ of the current trace if there is one. That is, clears the trace context only for the execution of fn, so fn runs without knowledge of the current trace.
 
@@ -457,7 +455,6 @@ This is less likely to be used outside of instrumentation than `bindFunctionToTr
 can be useful if there are calls to instrumented libraries that you explicitly do not want to show up in your traces. For instance, the beeline uses this function so that the http POST the beeline makes to honeycomb's api is not included in the trace.
 
 example
-
 ```javascript
 // trace active at this point
 
diff --git a/lib/api/libhoney.js b/lib/api/libhoney.js
index b26507c..dc576b7 100644
--- a/lib/api/libhoney.js
+++ b/lib/api/libhoney.js
@@ -112,13 +112,13 @@ module.exports = class LibhoneyEventAPI {
   startAsyncSpan(metadataContext, spanFn) {
     let parentId;
     let spanId = uuidv4();
-    let context = tracker.getTracked();
-    if (context.stack.length > 0) {
-      parentId = context.stack[context.stack.length - 1][schema.TRACE_SPAN_ID];
+    let parentContext = tracker.getTracked();
+    if (parentContext.stack.length > 0) {
+      parentId = parentContext.stack[parentContext.stack.length - 1][schema.TRACE_SPAN_ID];
     }
 
     const eventPayload = Object.assign({}, metadataContext, {
-      [schema.TRACE_ID]: context.id,
+      [schema.TRACE_ID]: parentContext.id,
       [schema.TRACE_SPAN_ID]: spanId,
       startTime: Date.now(),
       startTimeHR: process.hrtime(),
@@ -128,8 +128,8 @@ module.exports = class LibhoneyEventAPI {
     }
 
     let newContext = {
-      id: context.id,
-      customContext: {},
+      id: parentContext.id,
+      customContext: parentContext.customContext,
       stack: [eventPayload],
     };
 


Test patch:
diff --git a/lib/api/index.test.js b/lib/api/index.test.js
index d9605a6..d85f4c8 100644
--- a/lib/api/index.test.js
+++ b/lib/api/index.test.js
@@ -7,7 +7,8 @@ const path = require("path"),
   pkg = require(path.join(__dirname, "..", "..", "package.json"));
 
 beforeEach(() =>
-  event.configure({ impl: "libhoney-event", transmission: "mock", writeKey: "abc123" }));
+  event.configure({ impl: "libhoney-event", transmission: "mock", writeKey: "abc123" })
+);
 afterEach(() => event._resetForTesting());
 test("libhoney default config", () => {
   const honey = event._apiForTesting().honey;
@@ -371,3 +372,23 @@ test("distributed tracing linkage is correct", () => {
   s.add(rootEventData[schema.TRACE_SPAN_ID]);
   expect(s.size).toBe(3);
 });
+
+test("async spans share custom context", () => {
+  const honey = event._apiForTesting().honey;
+
+  let rootSpan = event.startTrace({
+    [schema.TRACE_SPAN_NAME]: "root",
+  });
+
+  event.customContext.add("custom", "value");
+
+  event.startAsyncSpan({}, span => {
+    event.finishSpan(span);
+  });
+  event.finishTrace(rootSpan);
+
+  expect(honey.transmission.events.length).toBe(2);
+
+  let asyncData = JSON.parse(honey.transmission.events[0].postData);
+  expect(asyncData["app.custom"]).toBe("value");
+});