Repository: ViacomInc/data-point (js)

Issue:
create tracing api
<!--
Thanks for your interest in the project. We appreciate bugs filed and PRs submitted!
Please make sure that you are familiar with and follow the Code of Conduct for
this project (found in the CODE_OF_CONDUCT.md file).

Please fill/un-comment out this template with all the relevant information so we can
understand what's going on and fix the issue.
-->

**Problem description**:

We need an API that will allow us to be agnostic of the tracing solution the user wants to use. 

**Suggested solution**:

Create an API  where we can expose a place to call `start`, `error` and `finish` tracing spans, so others can implement a tracing platform of their choice.


**Environment**:

- `data-point` version:  v6 canary

Pull request description:
feat(data-point): create tracing api
<!--
Thanks for your interest in the project. We appreciate bugs filed and PRs submitted!

Please make sure that you are familiar with and follow the Code of Conduct for
this project (found in the CODE_OF_CONDUCT.md file).

Also, please make sure you're familiar with and follow the instructions in the
contributing guidelines (found in the CONTRIBUTING.md file).

Please fill out the information below to expedite the review and (hopefully)
merge of your pull request!

BREAKING CHANGES:

If your PR includes a breaking change, please submit it with a codemod under
data-point-codemods that will help users upgrade their codebase.

Breaking changes without a codemod will not be accepted unless a codemod is not
viable or does not apply to the specific situation.
-->

<!-- What changes are being made? (What feature/bug is being fixed here?) -->
**What**:

closes #416

<!-- Have you done all of these things?  -->
**Checklist**:
<!-- add "N/A" to the end of each line that's irrelevant to your changes -->
<!-- to check an item, place an "x" in the box like so: "- [x] Documentation" -->
- [ ] Has Breaking changes
- [ ] Documentation
- [X] Tests
- [X] Ready to be merged <!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
- [ ] Added username to **all-contributors** list

<!-- feel free to add additional comments -->

Patch:
diff --git a/documentation/docs/playground.js b/documentation/docs/playground.js
index bda67dd..c179e48 100644
--- a/documentation/docs/playground.js
+++ b/documentation/docs/playground.js
@@ -4,7 +4,6 @@ const DataPoint = require("@data-point/core");
 const DPModel = require("@data-point/core/model");
 const DPIfThenElse = require("@data-point/core/ifThenElse");
 const DPMap = require("@data-point/core/map");
-const DPTracer = require("@data-point/tracer");
 
 const fs = require("fs");
 
@@ -70,34 +69,9 @@ async function main() {
     }
   ];
 
-  const tracer = new DPTracer();
-
-  const span = tracer.startSpan("data-point-request");
-
   console.time("dp1");
-  result = await datapoint.resolve(input, DPMap(myModel), {
-    // tracer: span
-  });
+  result = await datapoint.resolve(input, DPMap(myModel));
   console.timeEnd("dp1");
-
-  span.finish();
-
-  console.time("dp2");
-  result = await datapoint.resolve(input, DPMap(myModel), {
-    // tracer: span
-  });
-  console.timeEnd("dp2");
-
-  // span2.finish();
-
-  // fs.writeFileSync(
-  //   "/Users/pacheca/Downloads/tracing.json",
-  //   JSON.stringify(tracer.report("chrome-tracing"))
-  // );
-
-  console.log(tracer.report("chrome-tracing"));
-  console.log(store);
-  console.log(result);
 }
 
 main();
diff --git a/packages/data-point-tracer/index.js b/packages/data-point-tracer/index.js
deleted file mode 100644
index abd3c93..0000000
--- a/packages/data-point-tracer/index.js
+++ /dev/null
@@ -1,1 +0,0 @@
-module.exports = require("./src/Tracer");
diff --git a/packages/data-point-tracer/package.json b/packages/data-point-tracer/package.json
deleted file mode 100644
index 052a6ad..0000000
--- a/packages/data-point-tracer/package.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
-  "name": "@data-point/tracer",
-  "version": "0.0.0",
-  "description": "Simple tracer",
-  "main": "index.js",
-  "private": true,
-  "license": "Apache-2.0",
-  "homepage": "https://github.com/ViacomInc/data-point",
-  "repository": {
-    "type": "git",
-    "url": "https://github.com/ViacomInc/data-point.git"
-  },
-  "bugs": {
-    "url": "https://github.com/ViacomInc/data-point/issues"
-  },
-  "engines": {
-    "node": ">=8",
-    "npm": ">=2"
-  },
-  "author": {
-    "name": "Acatl Pacheco",
-    "email": "acatl.pacheco@viacom.com"
-  }
-}
diff --git a/packages/data-point-tracer/src/Span.js b/packages/data-point-tracer/src/Span.js
deleted file mode 100644
index 2dff17e..0000000
--- a/packages/data-point-tracer/src/Span.js
+++ /dev/null
@@ -1,52 +0,0 @@
-function toMS(hrtime) {
-  const nanoseconds = hrtime[0] * 1e9 + hrtime[1];
-  // return nanoseconds / 1e6;
-  return nanoseconds / 1000;
-}
-
-class Span {
-  constructor(name, options = {}) {
-    this.spans = [];
-    this.name = name;
-    this.tags = new Map();
-    this.__log = [];
-    this.hrtimeStart = process.hrtime();
-    this.childOf = options.childOf;
-    this.tracer = null;
-  }
-
-  setTag(name, value) {
-    this.tags.set(name, value);
-    return this;
-  }
-
-  log(logObject) {
-    this.__log.push(logObject);
-  }
-
-  finish() {
-    this.hrtimeDuration = process.hrtime(this.hrtimeStart);
-  }
-
-  startSpan(name, options) {
-    const span = new Span(name, options);
-    span.tracer = this.tracer;
-
-    span.tracer.spans.push(span);
-    return span;
-  }
-
-  toJSON() {
-    return {
-      pid: this.tags.get("pid") || 0,
-      name: this.name,
-      start: toMS(this.hrtimeStart),
-      duration: toMS(this.hrtimeDuration),
-      tags: this.tags,
-      log: this.__log,
-      childOf: this.childOf
-    };
-  }
-}
-
-module.exports = Span;
diff --git a/packages/data-point-tracer/src/Tracer.js b/packages/data-point-tracer/src/Tracer.js
deleted file mode 100644
index 30f4188..0000000
--- a/packages/data-point-tracer/src/Tracer.js
+++ /dev/null
@@ -1,30 +0,0 @@
-const Span = require("./Span");
-const reporters = require("./reporters");
-
-class Tracer {
-  constructor() {
-    this.spans = [];
-  }
-
-  startSpan(name, options) {
-    const span = new Span(name, options);
-    span.tracer = this;
-
-    this.spans.push(span);
-    return span;
-  }
-
-  report(reportType) {
-    if (this.spans.length === 0) {
-      return [];
-    }
-    switch (reportType) {
-      case "chrome-tracing":
-        return reporters.chromeTracer(this.spans);
-      default:
-        return reporters.toJSON(this.spans);
-    }
-  }
-}
-
-module.exports = Tracer;
diff --git a/packages/data-point-tracer/src/reporters.js b/packages/data-point-tracer/src/reporters.js
deleted file mode 100644
index 17538fc..0000000
--- a/packages/data-point-tracer/src/reporters.js
+++ /dev/null
@@ -1,49 +0,0 @@
-function toJSON(spans) {
-  return spans.map(span => {
-    return span.toJSON();
-  });
-}
-
-function chromeTracer(spans) {
-  const firstSpan = spans[0].toJSON();
-  const timeStart = firstSpan.start;
-  const traceEvents = spans.reduce((report, span) => {
-    const spanJson = span.toJSON();
-    report.push(
-      // {
-      //   cat: "data-point",
-      //   pid: spanJson.pid,
-      //   name: spanJson.name,
-      //   ph: "b",
-      //   ts: spanJson.start - timeStart,
-      //   id: 0x100
-      // },
-      // {
-      //   cat: "data-point",
-      //   pid: spanJson.pid,
-      //   name: spanJson.name,
-      //   ph: "e",
-      //   ts: spanJson.duration,
-      //   id: 0x100
-      // },
-      {
-        pid: spanJson.pid,
-        tid: 1,
-        ts: spanJson.start - timeStart,
-        dur: spanJson.duration,
-        ph: "X",
-        name: `[${spanJson.pid}]${spanJson.name}`
-      }
-    );
-    return report;
-  }, []);
-
-  return {
-    traceEvents
-  };
-}
-
-module.exports = {
-  toJSON,
-  chromeTracer
-};
diff --git a/packages/data-point/package.json b/packages/data-point/package.json
index 4f30e0b..7a4b64d 100644
--- a/packages/data-point/package.json
+++ b/packages/data-point/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@data-point/core",
-  "version": "3.4.3",
+  "version": "6.0.0",
   "description": "Data Processing and Transformation Utility",
   "main": "index.js",
   "private": true,
diff --git a/packages/data-point/src/DataPoint.js b/packages/data-point/src/DataPoint.js
index c6c0ec5..1346344 100644
--- a/packages/data-point/src/DataPoint.js
+++ b/packages/data-point/src/DataPoint.js
@@ -4,6 +4,8 @@ const { resolve } = require("./resolve");
 const { Cache } = require("./Cache");
 const isPlainObject = require("./is-plain-object");
 
+const Tracer = require("./tracing/Tracer");
+
 /**
  * Applies a reducer from an accumulator object.
  *
@@ -13,7 +15,7 @@ const isPlainObject = require("./is-plain-object");
  */
 async function resolveFromAccumulator(acc, reducer) {
   const parsedReducers = createReducer(reducer);
-  return resolve(acc, parsedReducers);
+  return resolve(acc, parsedReducers, true);
 }
 
 /**
@@ -25,8 +27,8 @@ async function resolveFromAccumulator(acc, reducer) {
  * @param {Cache|undefined} options.cache cache manager, see Cache for options.
  * @param {Object|undefined} options.locals persistent object that is
  * accessible via the Accumulator object on every reducer.
- * @param {OpenTrace.Span|undefined} options.tracer when provided it should
- * comply with the **opentracing** Span API.
+ * @param {Tracer} options.tracer when provided it should
+ * comply with the DataPoint Tracing API.
  * @returns {Promise<any>} resolved value
  */
 async function resolveFromInput(input, reducer, options = {}) {
@@ -55,29 +57,22 @@ function validateLocals(locals) {
 }
 
 /**
- * @param {OpenTrace.Span} span when provided it should
- * comply with the **opentracing** Span API.
- * @throws Error if the object does not expose the methods `startSpan`,
- * `setTag`, `log`.
+ * @param {Tracer} options.tracer when provided it should
+ * comply with the DataPoint Tracing API.
+ * @throws Error if the object does not expose the methods `start`.
  */
-function validateTracingSpan(span) {
-  if (span) {
-    if (typeof span.startSpan !== "function") {
-      throw new Error(
-        "tracer.startSpan must be a function, tracer expects opentracing API (see https://opentracing.io)"
-      );
+function validateTracer(tracer) {
+  if (tracer) {
+    if (typeof tracer.start !== "function") {
+      throw new Error("tracer.start must be a function");
     }
 
-    if (typeof span.setTag !== "function") {
-      throw new Error(
-        "tracer.setTag must be a function, tracer expects opentracing API (see https://opentracing.io)"
-      );
+    if (tracer.error && typeof tracer.error !== "function") {
+      throw new Error("tracer.error must be a function");
     }
 
-    if (typeof span.log !== "function") {
-      throw new Error(
-        "tracer.log must be a function, tracer expects opentracing API (see https://opentracing.io)"
-      );
+    if (tracer.finish && typeof tracer.finish !== "function") {
+      throw new Error("tracer.finish must be a function");
     }
   }
 }
@@ -100,18 +95,24 @@ class DataPoint {
    * @param {Object} options
    * @param {Object|undefined} options.locals persistent object that is
    * accessible via the Accumulator object on every reducer.
-   * @param {OpenTrace.Span|undefined} options.tracer when provided it should
-   * comply with the **opentracing** Span API.
+   * @param {Tracer} options.tracer when provided it should
+   * comply with the DataPoint Tracing API.
    * @returns {Promise<any>} result from running the input thru the
    * provided reducer.
    */
   async resolve(input, reducer, options = {}) {
     validateLocals(options.locals);
-    validateTracingSpan(options.tracer);
+    validateTracer(options.tracer);
+
+    let tracer;
+    if (options.tracer) {
+      tracer = new Tracer(options.tracer);
+    }
 
     return resolveFromInput(input, reducer, {
       ...options,
-      cache: this.cache
+      cache: this.cache,
+      tracer
     });
   }
 }
@@ -121,5 +122,5 @@ module.exports = {
   resolveFromInput,
   DataPoint,
   validateLocals,
-  validateTracingSpan
+  validateTracer
 };
diff --git a/packages/data-point/src/ReducerEntity.js b/packages/data-point/src/ReducerEntity.js
index 6c9fdc1..3d3b762 100644
--- a/packages/data-point/src/ReducerEntity.js
+++ b/packages/data-point/src/ReducerEntity.js
@@ -79,19 +79,15 @@ class ReducerEntity extends Reducer {
     try {
       acc.value = await this.resolveEntityValue(acc, resolveReducer);
     } catch (error) {
-      error.reducer = this;
-
-      acc = acc.set("value", error);
-
-      if (this.catch) {
-        acc = acc.set("value", await resolveReducer(acc, this.catch));
-        if (this.outputType) {
-          await resolveReducer(acc, this.outputType);
-        }
-        return acc.value;
+      if (!this.catch) {
+        throw error;
       }
 
-      throw error;
+      acc = acc.set("value", await resolveReducer(acc, this.catch));
+      if (this.outputType) {
+        await resolveReducer(acc, this.outputType);
+      }
+      return acc.value;
     }
 
     return acc.value;
diff --git a/packages/data-point/src/resolve.js b/packages/data-point/src/resolve.js
index f0c683a..6e6fb70 100644
--- a/packages/data-point/src/resolve.js
+++ b/packages/data-point/src/resolve.js
@@ -1,7 +1,8 @@
 const uniqueIdScope = require("./unique-id-scope");
-const traceSpan = require("./trace-span");
+const traceSpan = require("./tracing/trace-span");
 
 const pid = uniqueIdScope();
+const createThreadId = uniqueIdScope();
 
 /**
  * Applies a Reducer to an accumulator
@@ -10,12 +11,17 @@ const pid = uniqueIdScope();
  * @param {Reducer} reducer
  * @returns {Promise}
  */
-async function resolve(accumulator, reducer) {
+async function resolve(accumulator, reducer, newThread = false) {
   const acc = accumulator.create();
   acc.reducer = reducer;
+
+  if (newThread) {
+    acc.threadId = createThreadId();
+  }
+
   acc.pid = pid();
 
-  const span = traceSpan.create(acc);
+  const span = traceSpan.start(acc, accumulator.reducer);
 
   let result;
   try {
@@ -28,7 +34,13 @@ async function resolve(accumulator, reducer) {
       error.reducerStackTrace = acc.reducerStackTrace;
     }
 
-    traceSpan.logError(span, error);
+    // only set error.reducer once so any following catch does not override the
+    // original reducer.
+    if (!error.reducer) {
+      error.reducer = reducer;
+    }
+
+    traceSpan.error(span, error);
 
     // rethrow the error up
     throw error;
diff --git a/packages/data-point/src/trace-span.js b/packages/data-point/src/trace-span.js
deleted file mode 100644
index 466c8c8..0000000
--- a/packages/data-point/src/trace-span.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * NOTE: mutates accumulator
- * @private
- * @param {*} accumulator
- */
-function create(accumulator) {
-  if (!accumulator.tracer || !accumulator.tracer.startSpan) {
-    return undefined;
-  }
-
-  const span = accumulator.tracer.startSpan(accumulator.reducer.id, {
-    childOf: accumulator.tracer
-  });
-
-  // passing reference to accumulator to create child-parent relationship
-  accumulator.tracer = span;
-
-  span.setTag("pid", accumulator.pid);
-
-  return span;
-}
-
-function logError(span, error) {
-  if (!span || !span.log) {
-    return;
-  }
-
-  span.log({
-    event: "error",
-    "error.object": error,
-    message: error.message,
-    stack: error.stack
-  });
-}
-
-function finish(span) {
-  if (!span || !span.finish) {
-    return;
-  }
-
-  span.finish();
-}
-
-module.exports = {
-  create,
-  logError,
-  finish
-};
diff --git a/packages/data-point/src/tracing/Span.js b/packages/data-point/src/tracing/Span.js
new file mode 100644
index 0000000..d8db893
--- /dev/null
+++ b/packages/data-point/src/tracing/Span.js
@@ -0,0 +1,51 @@
+function toNanoSeconds(hrtime) {
+  return hrtime[0] * 1e9 + hrtime[1];
+}
+
+function getHighResolutionTime() {
+  return toNanoSeconds(process.hrtime());
+}
+
+/**
+ * @alias Span
+ */
+class Span {
+  constructor(name, options, tracer) {
+    this.root = false;
+    this.name = name;
+    this.parent = options.parent;
+    this.context = options.context;
+    this.tracer = tracer;
+    this.timeStartNs = getHighResolutionTime();
+    this.timeEndNs = undefined;
+    this.errors = undefined;
+    this.data = {};
+  }
+
+  start(name, options) {
+    const span = new Span(name, options, this.tracer);
+    this.tracer.handlers.start(span);
+    return span;
+  }
+
+  setError(err) {
+    this.error = err;
+    if (typeof this.tracer.handlers.error === "function") {
+      this.tracer.handlers.error(this, err);
+    }
+  }
+
+  finish() {
+    this.timeEndNs = getHighResolutionTime();
+
+    if (typeof this.tracer.handlers.finish === "function") {
+      this.tracer.handlers.finish(this);
+    }
+  }
+}
+
+module.exports = {
+  toNanoSeconds,
+  getHighResolutionTime,
+  Span
+};
diff --git a/packages/data-point/src/tracing/Tracer.js b/packages/data-point/src/tracing/Tracer.js
new file mode 100644
index 0000000..2418c7b
--- /dev/null
+++ b/packages/data-point/src/tracing/Tracer.js
@@ -0,0 +1,17 @@
+const { Span } = require("./Span");
+
+class Tracer {
+  constructor(handlers) {
+    this.handlers = handlers;
+  }
+
+  start(name, options) {
+    const span = new Span(name, options, this);
+    span.root = true;
+
+    this.handlers.start(span);
+    return span;
+  }
+}
+
+module.exports = Tracer;
diff --git a/packages/data-point/src/tracing/trace-span.js b/packages/data-point/src/tracing/trace-span.js
new file mode 100644
index 0000000..a97d72c
--- /dev/null
+++ b/packages/data-point/src/tracing/trace-span.js
@@ -0,0 +1,50 @@
+/**
+ * NOTE: mutates accumulator
+ * @private
+ * @param {*} accumulator
+ */
+function start(accumulator) {
+  if (!accumulator.tracer) {
+    return undefined;
+  }
+
+  const span = accumulator.tracer.start(accumulator.reducer.id, {
+    context: accumulator,
+    parent: accumulator.tracer
+  });
+
+  // passing reference to accumulator to create child-parent relationship
+  accumulator.tracer = span;
+
+  return span;
+}
+/**
+ * @private
+ * @param {Span} span
+ * @param {Error} spanError
+ */
+function error(span, spanError) {
+  if (!span) {
+    return;
+  }
+
+  span.setError(spanError);
+}
+
+/**
+ * @private
+ * @param {Span} span
+ */
+function finish(span) {
+  if (!span) {
+    return;
+  }
+
+  span.finish();
+}
+
+module.exports = {
+  start,
+  error,
+  finish
+};


Test patch:
diff --git a/packages/data-point/src/DataPoint.test.js b/packages/data-point/src/DataPoint.test.js
index eead88a..99309a5 100644
--- a/packages/data-point/src/DataPoint.test.js
+++ b/packages/data-point/src/DataPoint.test.js
@@ -6,10 +6,9 @@ const sayHello = value => `Hello ${value}`;
 const getAccumulator = (input, acc) => acc;
 
 const shallowTracer = {
-  // resolve will call this method, which expects a Span object in return
-  startSpan: () => shallowTracer,
-  setTag: () => true,
-  log: () => true
+  start: () => shallowTracer,
+  error: () => true,
+  finish: () => true
 };
 
 describe("resolveFromAccumulator", () => {
@@ -34,8 +33,7 @@ describe("resolveFromInput", () => {
       cache: {
         get: () => true,
         set: () => true
-      },
-      tracer: "tracer"
+      }
     };
     it("should pass locals object", async () => {
       const result = await dataPoint.resolveFromInput(
@@ -113,40 +111,35 @@ describe("validateLocals", () => {
   });
 });
 
-describe("validateTracingSpan", () => {
+describe("validateTracer", () => {
   it("should only allow undefined or well defined tracer span API", () => {
     expect(() => {
-      dataPoint.validateTracingSpan();
+      dataPoint.validateTracer();
     }).not.toThrow();
 
     expect(() => {
-      dataPoint.validateTracingSpan(shallowTracer);
+      dataPoint.validateTracer(shallowTracer);
     }).not.toThrow();
   });
 
   it("should throw error on any un-valid value", () => {
     expect(() => {
-      dataPoint.validateTracingSpan({});
-    }).toThrowErrorMatchingInlineSnapshot(
-      `"tracer.startSpan must be a function, tracer expects opentracing API (see https://opentracing.io)"`
-    );
+      dataPoint.validateTracer({});
+    }).toThrowErrorMatchingInlineSnapshot(`"tracer.start must be a function"`);
 
     expect(() => {
-      dataPoint.validateTracingSpan({
-        startSpan: () => true
+      dataPoint.validateTracer({
+        start: () => true,
+        error: "invalid"
       });
-    }).toThrowErrorMatchingInlineSnapshot(
-      `"tracer.setTag must be a function, tracer expects opentracing API (see https://opentracing.io)"`
-    );
+    }).toThrowErrorMatchingInlineSnapshot(`"tracer.error must be a function"`);
 
     expect(() => {
-      dataPoint.validateTracingSpan({
-        startSpan: () => true,
-        setTag: () => true
+      dataPoint.validateTracer({
+        start: () => true,
+        finish: "invalid"
       });
-    }).toThrowErrorMatchingInlineSnapshot(
-      `"tracer.log must be a function, tracer expects opentracing API (see https://opentracing.io)"`
-    );
+    }).toThrowErrorMatchingInlineSnapshot(`"tracer.finish must be a function"`);
   });
 });
 
diff --git a/packages/data-point/src/ReducerEntity.test.js b/packages/data-point/src/ReducerEntity.test.js
index 6020f30..8f22617 100644
--- a/packages/data-point/src/ReducerEntity.test.js
+++ b/packages/data-point/src/ReducerEntity.test.js
@@ -139,7 +139,6 @@ describe("ReducerEntity", () => {
         }
 
         expect(error).toMatchInlineSnapshot(`[Error: entityError]`);
-        expect(error.reducer).toEqual(entity);
       });
 
       it("should resolve this.catch", async () => {
diff --git a/packages/data-point/src/resolve.test.js b/packages/data-point/src/resolve.test.js
index 5869892..a52e574 100644
--- a/packages/data-point/src/resolve.test.js
+++ b/packages/data-point/src/resolve.test.js
@@ -4,11 +4,11 @@ jest.mock("./unique-id-scope", () => {
   return () => mockPid;
 });
 
-jest.mock("./trace-span");
+jest.mock("./tracing/trace-span");
 
 const { resolve } = require("./resolve");
 const { Accumulator } = require("./Accumulator");
-const traceSpan = require("./trace-span");
+const traceSpan = require("./tracing/trace-span");
 
 const reducer = {
   resolveReducer: jest.fn(() => "resolved")
@@ -70,18 +70,53 @@ describe("resolve", () => {
     });
   });
 
+  describe("error.reducer", () => {
+    it("should augment error with reducer", async () => {
+      const acc = new Accumulator();
+
+      const testError = new Error("resolveReducer failed");
+
+      const reducerRejected = {
+        id: "badReducer",
+        resolveReducer: jest.fn().mockRejectedValue(testError)
+      };
+
+      const result = await resolve(acc, reducerRejected).catch(error => error);
+
+      expect(result).toBeInstanceOf(Error);
+      expect(result.reducer).toEqual(reducerRejected);
+    });
+
+    it("should not augment error with reducer if already set", async () => {
+      const acc = new Accumulator();
+
+      const testError = new Error("resolveReducer failed");
+      testError.reducer = "initialReducer";
+
+      const reducerRejected = {
+        id: "badReducer",
+        resolveReducer: jest.fn().mockRejectedValue(testError)
+      };
+
+      const result = await resolve(acc, reducerRejected).catch(error => error);
+
+      expect(result).toBeInstanceOf(Error);
+      expect(result.reducer).toEqual("initialReducer");
+    });
+  });
+
   describe("tracing", () => {
-    it("should call traceSpan.create with accumulator", async () => {
+    it("should call traceSpan.start with accumulator", async () => {
       const acc = new Accumulator();
       await resolve(acc, reducer);
-      const [accParam] = traceSpan.create.mock.calls[0];
+      const [accParam] = traceSpan.start.mock.calls[0];
       expect(accParam).toMatchObject({
         pid: "pid",
         reducer
       });
     });
 
-    it("should call traceSpan.logError when resolveReducer throws error", async () => {
+    it("should call traceSpan.error when resolveReducer throws error", async () => {
       const acc = new Accumulator();
 
       const testError = new Error("resolveReducer failed");
@@ -91,9 +126,9 @@ describe("resolve", () => {
 
       await resolve(acc, reducerRejected).catch(error => error);
 
-      expect(traceSpan.logError).toBeCalled();
+      expect(traceSpan.error).toBeCalled();
 
-      expect(traceSpan.logError.mock.calls[0]).toEqual([undefined, testError]);
+      expect(traceSpan.error.mock.calls[0]).toEqual([undefined, testError]);
     });
 
     it("should finally call traceSpan.finish()", async () => {
diff --git a/packages/data-point/src/trace-span.test.js b/packages/data-point/src/trace-span.test.js
deleted file mode 100644
index 69ddd47..0000000
--- a/packages/data-point/src/trace-span.test.js
+++ /dev/null
@@ -1,110 +0,0 @@
-const traceSpan = require("./trace-span");
-const { Accumulator } = require("./Accumulator");
-
-describe("create", () => {
-  it("should return undefined if tracer is not defined", () => {
-    const acc = new Accumulator();
-    expect(traceSpan.create(acc)).toEqual(undefined);
-  });
-
-  it("should return undefined if tracer.startSpan is not defined", () => {
-    const acc = new Accumulator();
-    acc.tracer = {};
-    expect(traceSpan.create(acc)).toEqual(undefined);
-  });
-
-  describe("create span when defined", () => {
-    // NOTE: grouping a couple of expect srtatements here to keep this tests
-    // simple
-    it("should create new span", () => {
-      const acc = new Accumulator();
-
-      acc.pid = "accPId";
-
-      acc.reducer = {
-        id: "reducerId"
-      };
-
-      const span = {
-        setTag: jest.fn()
-      };
-
-      const tracer = {
-        startSpan: jest.fn(() => span)
-      };
-
-      acc.tracer = tracer;
-
-      // should return span
-      expect(traceSpan.create(acc)).toEqual(span);
-      // should set acc.tracer to new span
-      expect(acc.tracer).toEqual(span);
-
-      // tracer.startSpan should follow opentracing API
-      expect(tracer.startSpan).toBeCalled();
-      const [arg1, arg2] = tracer.startSpan.mock.calls[0];
-      expect(arg1).toEqual("reducerId");
-      // should set childOf relationship
-      expect(arg2).toMatchObject({
-        childOf: tracer
-      });
-
-      expect(span.setTag).toBeCalledWith("pid", "accPId");
-    });
-  });
-});
-
-describe("logError", () => {
-  it("should do nothing if span is not set", () => {
-    expect(() => {
-      traceSpan.logError(undefined);
-    }).not.toThrowError();
-  });
-
-  it("should do nothing if span.log is not set", () => {
-    const span = {};
-    expect(() => {
-      traceSpan.logError(span);
-    }).not.toThrowError();
-  });
-
-  it("should call span.log with error information", () => {
-    const span = {
-      log: jest.fn()
-    };
-    const error = new Error("test");
-    traceSpan.logError(span, error);
-    expect(span.log).toHaveBeenCalled();
-    const arg1 = span.log.mock.calls[0][0];
-    expect(arg1).toMatchObject({
-      event: "error",
-      "error.object": error,
-      message: "test",
-      stack: error.stack
-    });
-  });
-});
-
-describe("finish", () => {
-  it("should do nothing if span is not set", () => {
-    const span = undefined;
-    expect(() => {
-      traceSpan.finish(span);
-    }).not.toThrowError();
-  });
-
-  it("should do nothing if span.finish is not set", () => {
-    const span = {};
-    expect(() => {
-      traceSpan.finish(span);
-    }).not.toThrowError();
-  });
-
-  it("should call span.finish with error information", () => {
-    const span = {
-      finish: jest.fn()
-    };
-    traceSpan.finish(span);
-    expect(span.finish).toHaveBeenCalled();
-  });
-});
diff --git a/packages/data-point/src/tracing/Span.test.js b/packages/data-point/src/tracing/Span.test.js
new file mode 100644
index 0000000..5b7a2e9
--- /dev/null
+++ b/packages/data-point/src/tracing/Span.test.js
@@ -0,0 +1,121 @@
+const { toNanoSeconds, getHighResolutionTime, Span } = require("./Span");
+
+const handlers = {
+  start: jest.fn()
+};
+
+const tracer = {
+  handlers
+};
+
+afterEach(() => {
+  jest.clearAllMocks();
+});
+
+const mockHrtimeResult = [75238, 684529285];
+const mockHighResolutionTime = 75238684529285;
+
+describe("toNanoSeconds", () => {
+  it("should convert hrtime to nanoseconds", () => {
+    expect(toNanoSeconds(mockHrtimeResult)).toEqual(mockHighResolutionTime);
+  });
+});
+
+describe("getHighResolutionTime", () => {
+  it("should get nanoseconds from current hrtime", () => {
+    const spyProcessHrtime = jest
+      .spyOn(process, "hrtime")
+      .mockReturnValue(mockHrtimeResult);
+
+    expect(getHighResolutionTime()).toEqual(mockHighResolutionTime);
+
+    spyProcessHrtime.mockRestore();
+  });
+});
+
+describe("Span", () => {
+  describe("constructor", () => {
+    it("should set name", () => {
+      expect(new Span("name", {}, tracer)).toHaveProperty("name", "name");
+    });
+
+    it("should set tracer", () => {
+      expect(new Span("name", {}, tracer)).toHaveProperty("tracer", tracer);
+    });
+
+    it("should set timestamps", () => {
+      const spyProcessHrtime = jest
+        .spyOn(process, "hrtime")
+        .mockReturnValue(mockHrtimeResult);
+
+      const span = new Span("name", {}, tracer);
+      expect(span).toHaveProperty("timeStartNs", mockHighResolutionTime);
+      expect(span).toHaveProperty("timeEndNs", undefined);
+
+      spyProcessHrtime.mockRestore();
+    });
+  });
+
+  describe("start", () => {
+    it("should create a span and call handlers.start", () => {
+      const span = new Span("name", {}, tracer);
+      const newSpan = span.start("newSpan", {
+        context: {},
+        parent: {}
+      });
+      expect(newSpan).toBeInstanceOf(Span);
+      expect(newSpan).toHaveProperty("name", "newSpan");
+      expect(newSpan).toHaveProperty("tracer", tracer);
+      expect(handlers.start).toHaveBeenCalledWith(newSpan);
+    });
+  });
+
+  describe("setError", () => {
+    it("should set error", () => {
+      const span = new Span("name", {}, tracer);
+      const error = new Error("test");
+      span.setError(error);
+      expect(span).toHaveProperty("error", error);
+    });
+
+    it("should call tracer.handlers.error if available", () => {
+      const tracerWithError = {
+        handlers: {
+          start: jest.fn(),
+          error: jest.fn()
+        }
+      };
+
+      const span = new Span("name", {}, tracerWithError);
+      const error = new Error("test");
+      span.setError(error);
+      expect(tracerWithError.handlers.error).toHaveBeenCalledWith(span, error);
+    });
+  });
+
+  describe("finish", () => {
+    it("should set timeEndNs", () => {
+      const span = new Span("name", {}, tracer);
+      const spyProcessHrtime = jest
+        .spyOn(process, "hrtime")
+        .mockReturnValue(mockHrtimeResult);
+
+      span.finish();
+      expect(span).toHaveProperty("timeEndNs", mockHighResolutionTime);
+      spyProcessHrtime.mockRestore();
+    });
+
+    it("should call tracer.handlers.finish if available", () => {
+      const tracerWithFinish = {
+        handlers: {
+          start: jest.fn(),
+          finish: jest.fn()
+        }
+      };
+
+      const span = new Span("name", {}, tracerWithFinish);
+      span.finish();
+      expect(tracerWithFinish.handlers.finish).toHaveBeenCalledWith(span);
+    });
+  });
+});
diff --git a/packages/data-point/src/tracing/Tracer.test.js b/packages/data-point/src/tracing/Tracer.test.js
new file mode 100644
index 0000000..76772e0
--- /dev/null
+++ b/packages/data-point/src/tracing/Tracer.test.js
@@ -0,0 +1,31 @@
+const Tracer = require("./Tracer");
+
+const handlers = {
+  start: jest.fn()
+};
+
+afterEach(() => {
+  jest.clearAllMocks();
+});
+
+describe("Tracer", () => {
+  describe("constructor", () => {
+    it("should set handlers", () => {
+      expect(new Tracer(handlers)).toHaveProperty("handlers", handlers);
+    });
+  });
+
+  describe("start", () => {
+    it("should create root span", () => {
+      const tracer = new Tracer(handlers);
+      const span = tracer.start("name", {});
+      expect(span).toHaveProperty("root", true);
+    });
+
+    it("should call handlers.start", () => {
+      const tracer = new Tracer(handlers);
+      const span = tracer.start("name", {});
+      expect(handlers.start).toBeCalledWith(span);
+    });
+  });
+});
diff --git a/packages/data-point/src/tracing/trace-span.test.js b/packages/data-point/src/tracing/trace-span.test.js
new file mode 100644
index 0000000..aad833e
--- /dev/null
+++ b/packages/data-point/src/tracing/trace-span.test.js
@@ -0,0 +1,77 @@
+const traceSpan = require("./trace-span");
+const { Accumulator } = require("../Accumulator");
+
+describe("start", () => {
+  it("should return undefined if tracer is not defined", () => {
+    const acc = new Accumulator();
+    expect(traceSpan.start(acc)).toEqual(undefined);
+  });
+
+  describe("start span when defined", () => {
+    // NOTE: grouping a couple of expect statements here to keep this tests
+    // simple
+    it("should start new span", () => {
+      const acc = new Accumulator();
+
+      acc.pid = "accPId";
+
+      acc.reducer = {
+        id: "reducerId"
+      };
+
+      const span = {
+        setTag: jest.fn()
+      };
+
+      const tracer = {
+        start: jest.fn(() => span)
+      };
+
+      acc.tracer = tracer;
+
+      // should return span
+      expect(traceSpan.start(acc)).toEqual(span);
+      // should set acc.tracer to new span
+      expect(acc.tracer).toEqual(span);
+
+      expect(tracer.start).toBeCalledWith("reducerId", {
+        context: acc,
+        parent: tracer
+      });
+    });
+  });
+});
+
+describe("error", () => {
+  it("should do nothing if span is not set", () => {
+    expect(() => {
+      traceSpan.error(undefined);
+    }).not.toThrowError();
+  });
+
+  it("should call span.setError with error object", () => {
+    const span = {
+      setError: jest.fn()
+    };
+    const error = new Error("test");
+    traceSpan.error(span, error);
+    expect(span.setError).toHaveBeenCalledWith(error);
+  });
+});
+
+describe("finish", () => {
+  it("should do nothing if span is not set", () => {
+    const span = undefined;
+    expect(() => {
+      traceSpan.finish(span);
+    }).not.toThrowError();
+  });
+
+  it("should call span.finish with error information", () => {
+    const span = {
+      finish: jest.fn()
+    };
+    traceSpan.finish(span);
+    expect(span.finish).toHaveBeenCalled();
+  });
+});