UNPKG

41.1 kBJavaScriptView Raw
1'use strict';
2
3function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
4
5function _interopNamespace(e) {
6 if (e && e.__esModule) { return e; } else {
7 var n = {};
8 if (e) {
9 Object.keys(e).forEach(function (k) {
10 var d = Object.getOwnPropertyDescriptor(e, k);
11 Object.defineProperty(n, k, d.get ? d : {
12 enumerable: true,
13 get: function () {
14 return e[k];
15 }
16 });
17 });
18 }
19 n['default'] = e;
20 return n;
21 }
22}
23
24var superagent = _interopDefault(require('superagent'));
25var url = _interopDefault(require('url'));
26var path = _interopDefault(require('path'));
27var events = require('events');
28
29var hasOwn = Object.prototype.hasOwnProperty;
30var toStr = Object.prototype.toString;
31var defineProperty = Object.defineProperty;
32var gOPD = Object.getOwnPropertyDescriptor;
33
34var isArray = function isArray(arr) {
35 if (typeof Array.isArray === 'function') {
36 return Array.isArray(arr);
37 }
38
39 return toStr.call(arr) === '[object Array]';
40};
41
42var isPlainObject = function isPlainObject(obj) {
43
44 if (!obj || toStr.call(obj) !== '[object Object]') {
45 return false;
46 }
47
48 var hasOwnConstructor = hasOwn.call(obj, 'constructor');
49 var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');
50 // Not own constructor property must be Object
51 if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) {
52 return false;
53 }
54
55 // Own properties are enumerated firstly, so to speed up,
56 // if last one is own, then all properties are own.
57 var key;
58 for (key in obj) { /**/ }
59
60 return typeof key === 'undefined' || hasOwn.call(obj, key);
61};
62
63// If name is '__proto__', and Object.defineProperty is available, define __proto__ as an own property on target
64var setProperty = function setProperty(target, options) {
65 if (defineProperty && options.name === '__proto__') {
66 defineProperty(target, options.name, {
67 enumerable: true,
68 configurable: true,
69 value: options.newValue,
70 writable: true
71 });
72 } else {
73 target[options.name] = options.newValue;
74 }
75};
76
77// Return undefined instead of __proto__ if '__proto__' is not an own property
78var getProperty = function getProperty(obj, name) {
79 if (name === '__proto__') {
80 if (!hasOwn.call(obj, name)) {
81 return void 0;
82 } else if (gOPD) {
83 // In early versions of node, obj['__proto__'] is buggy when obj has
84 // __proto__ as an own property. Object.getOwnPropertyDescriptor() works.
85 return gOPD(obj, name).value;
86 }
87 }
88
89 return obj[name];
90};
91
92var extend = function extend() {
93
94 var options, name, src, copy, copyIsArray, clone;
95 var target = arguments[0];
96 var i = 1;
97 var length = arguments.length;
98 var deep = false;
99
100 // Handle a deep copy situation
101 if (typeof target === 'boolean') {
102 deep = target;
103 target = arguments[1] || {};
104 // skip the boolean and the target
105 i = 2;
106 }
107 if (target == null || (typeof target !== 'object' && typeof target !== 'function')) {
108 target = {};
109 }
110
111 for (; i < length; ++i) {
112 options = arguments[i];
113 // Only deal with non-null/undefined values
114 if (options != null) {
115 // Extend the base object
116 for (name in options) {
117 src = getProperty(target, name);
118 copy = getProperty(options, name);
119
120 // Prevent never-ending loop
121 if (target !== copy) {
122 // Recurse if we're merging plain objects or arrays
123 if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) {
124 if (copyIsArray) {
125 copyIsArray = false;
126 clone = src && isArray(src) ? src : [];
127 } else {
128 clone = src && isPlainObject(src) ? src : {};
129 }
130
131 // Never move original objects, clone them
132 setProperty(target, { name: name, newValue: extend(deep, clone, copy) });
133
134 // Don't bring in undefined values
135 } else if (typeof copy !== 'undefined') {
136 setProperty(target, { name: name, newValue: copy });
137 }
138 }
139 }
140 }
141 }
142
143 // Return the modified object
144 return target;
145};
146
147/**
148 * Join two or more url pieces into one.
149 *
150 * Only the protocol/port/host in the first piece is saved,but all the get parameters
151 * will be saved.
152 *
153 * @param {String|Function}... Multiple url pieces in function or string type.
154 * @return {String} The URL joined.
155 */
156var urljoin = function urljoin() {
157
158 //convert to Array
159 var pieces = Array.prototype.slice.call(arguments);
160 var query = {};
161 var first, paths;
162
163 if (!pieces.length) {
164 return '';
165 } else if (1 === pieces.length) {
166 return pieces[0];
167 }
168
169 paths = pieces.map(function(piece) {
170 var pieceStr = 'function' === typeof piece ? piece() : String(piece || '');
171
172 if (!pieceStr) {
173 return '';
174 }
175
176 var parsed = url.parse(pieceStr, true);
177
178 if (!first && parsed) {
179 first = parsed;
180 }
181
182 extend(query, parsed.query);
183 return parsed.pathname;
184 }).filter(function(piece) {
185 return !!piece;
186 });
187
188 delete first.search; //we use query instead of search
189 first.query = query;
190 first.pathname = path.join.apply(path, paths).replace(new RegExp('\\' + path.sep, 'g'), '/');
191 return url.format(first);
192};
193
194// Copyright 2016 Hound Technology, Inc. All rights reserved.
195
196const USER_AGENT = "libhoney-js/2.1.0";
197
198const _global =
199 typeof window !== "undefined"
200 ? window
201 : typeof global !== "undefined"
202 ? global
203 : undefined;
204
205// how many events to collect in a batch
206const batchSizeTrigger = 50; // either when the eventQueue is > this length
207const batchTimeTrigger = 100; // or it's been more than this many ms since the first push
208
209// how many batches to maintain in parallel
210const maxConcurrentBatches = 10;
211
212// how many events to queue up for busy batches before we start dropping
213const pendingWorkCapacity = 10000;
214
215const emptyResponseCallback = function() {};
216
217const eachPromise = (arr, iteratorFn) =>
218 arr.reduce((p, item) => {
219 return p.then(() => {
220 return iteratorFn(item);
221 });
222 }, Promise.resolve());
223
224const partition = (arr, keyfn, createfn, addfn) => {
225 let result = Object.create(null);
226 arr.forEach(v => {
227 let key = keyfn(v);
228 if (!result[key]) {
229 result[key] = createfn(v);
230 } else {
231 addfn(result[key], v);
232 }
233 });
234 return result;
235};
236
237class BatchEndpointAggregator {
238 constructor(events) {
239 this.batches = partition(
240 events,
241 /* keyfn */
242 ev => `${ev.apiHost}_${ev.writeKey}_${ev.dataset}`,
243 /* createfn */
244 ev => ({
245 apiHost: ev.apiHost,
246 writeKey: ev.writeKey,
247 dataset: ev.dataset,
248 events: [ev]
249 }),
250 /* addfn */
251 (batch, ev) => batch.events.push(ev)
252 );
253 }
254
255 encodeBatchEvents(events) {
256 let first = true;
257 let numEncoded = 0;
258 let encodedEvents = events.reduce((acc, ev) => {
259 try {
260 let encodedEvent = ev.toJSON(); // directly call toJSON, not JSON.stringify, because the latter wraps it in an additional set of quotes
261 numEncoded++;
262 let newAcc = acc + (!first ? "," : "") + encodedEvent;
263 first = false;
264 return newAcc;
265 } catch (e) {
266 ev.encodeError = e;
267 return acc;
268 }
269 }, "");
270
271 let encoded = "[" + encodedEvents + "]";
272 return { encoded, numEncoded };
273 }
274}
275
276/**
277 * @private
278 */
279class ValidatedEvent {
280 constructor({
281 timestamp,
282 apiHost,
283 postData,
284 writeKey,
285 dataset,
286 sampleRate,
287 metadata
288 }) {
289 this.timestamp = timestamp;
290 this.apiHost = apiHost;
291 this.postData = postData;
292 this.writeKey = writeKey;
293 this.dataset = dataset;
294 this.sampleRate = sampleRate;
295 this.metadata = metadata;
296 }
297
298 toJSON() {
299 let fields = [];
300 if (this.timestamp) {
301 fields.push(`"time":${JSON.stringify(this.timestamp)}`);
302 }
303 if (this.sampleRate) {
304 fields.push(`"samplerate":${JSON.stringify(this.sampleRate)}`);
305 }
306 if (this.postData) {
307 fields.push(`"data":${this.postData}`);
308 }
309 return `{${fields.join(",")}}`;
310 }
311}
312
313class MockTransmission {
314 constructor(options) {
315 this.constructorArg = options;
316 this.events = [];
317 }
318
319 sendEvent(ev) {
320 this.events.push(ev);
321 }
322
323 sendPresampledEvent(ev) {
324 this.events.push(ev);
325 }
326
327 reset() {
328 this.constructorArg = null;
329 this.events = [];
330 }
331}
332
333class WriterTransmission {
334 sendEvent(ev) {
335 console.log(JSON.stringify(ev));
336 }
337
338 sendPresampledEvent(ev) {
339 console.log(JSON.stringify(ev));
340 }
341}
342
343class NullTransmission {
344 sendEvent(_ev) {}
345
346 sendPresampledEvent(_ev) {}
347}
348
349/**
350 * @private
351 */
352class Transmission {
353 constructor(options) {
354 this._responseCallback = emptyResponseCallback;
355 this._batchSizeTrigger = batchSizeTrigger;
356 this._batchTimeTrigger = batchTimeTrigger;
357 this._maxConcurrentBatches = maxConcurrentBatches;
358 this._pendingWorkCapacity = pendingWorkCapacity;
359 this._sendTimeoutId = -1;
360 this._eventQueue = [];
361 this._batchCount = 0;
362
363 if (typeof options.responseCallback === "function") {
364 this._responseCallback = options.responseCallback;
365 }
366 if (typeof options.batchSizeTrigger === "number") {
367 this._batchSizeTrigger = Math.max(options.batchSizeTrigger, 1);
368 }
369 if (typeof options.batchTimeTrigger === "number") {
370 this._batchTimeTrigger = options.batchTimeTrigger;
371 }
372 if (typeof options.maxConcurrentBatches === "number") {
373 this._maxConcurrentBatches = options.maxConcurrentBatches;
374 }
375 if (typeof options.pendingWorkCapacity === "number") {
376 this._pendingWorkCapacity = options.pendingWorkCapacity;
377 }
378
379 this._userAgentAddition = options.userAgentAddition || "";
380 this._proxy = options.proxy;
381
382 // Included for testing; to stub out randomness and verify that an event
383 // was dropped.
384 this._randomFn = Math.random;
385 }
386
387 _droppedCallback(ev, reason) {
388 this._responseCallback([
389 {
390 metadata: ev.metadata,
391 error: new Error(reason)
392 }
393 ]);
394 }
395
396 sendEvent(ev) {
397 // bail early if we aren't sampling this event
398 if (!this._shouldSendEvent(ev)) {
399 this._droppedCallback(ev, "event dropped due to sampling");
400 return;
401 }
402
403 this.sendPresampledEvent(ev);
404 }
405
406 sendPresampledEvent(ev) {
407 if (this._eventQueue.length >= this._pendingWorkCapacity) {
408 this._droppedCallback(ev, "queue overflow");
409 return;
410 }
411 this._eventQueue.push(ev);
412 if (this._eventQueue.length >= this._batchSizeTrigger) {
413 this._sendBatch();
414 } else {
415 this._ensureSendTimeout();
416 }
417 }
418
419 flush() {
420 if (this._eventQueue.length === 0 && this._batchCount === 0) {
421 // we're not currently waiting on anything, we're done!
422 return Promise.resolve();
423 }
424
425 return new Promise(resolve => {
426 this.flushCallback = () => {
427 this.flushCallback = null;
428 resolve();
429 };
430 });
431 }
432
433 _sendBatch() {
434 if (this._batchCount === maxConcurrentBatches) {
435 // don't start up another concurrent batch. the next timeout/sendEvent or batch completion
436 // will cause us to send another
437 return;
438 }
439
440 this._clearSendTimeout();
441
442 this._batchCount++;
443
444 let batchAgg = new BatchEndpointAggregator(
445 this._eventQueue.splice(0, this._batchSizeTrigger)
446 );
447
448 const finishBatch = () => {
449 this._batchCount--;
450
451 let queueLength = this._eventQueue.length;
452 if (queueLength > 0) {
453 if (queueLength >= this._batchSizeTrigger) {
454 this._sendBatch();
455 } else {
456 this._ensureSendTimeout();
457 }
458 return;
459 }
460
461 if (this._batchCount === 0 && this.flushCallback) {
462 this.flushCallback();
463 }
464 };
465
466 let batches = Object.keys(batchAgg.batches).map(k => batchAgg.batches[k]);
467 eachPromise(batches, batch => {
468 let url = urljoin(batch.apiHost, "/1/batch", batch.dataset);
469 let postReq = superagent.post(url);
470
471 let reqPromise;
472 {
473 reqPromise = Promise.resolve(
474 this._proxy
475 ? new Promise(function (resolve) { resolve(_interopNamespace(require('superagent-proxy'))); }).then(({ default: proxy }) => ({
476 req: proxy(postReq, this._proxy)
477 }))
478 : { req: postReq }
479 );
480 }
481 let { encoded, numEncoded } = batchAgg.encodeBatchEvents(batch.events);
482 return reqPromise.then(
483 ({ req }) =>
484 new Promise(resolve => {
485 // if we failed to encode any of the events, no point in sending anything to honeycomb
486 if (numEncoded === 0) {
487 this._responseCallback(
488 batch.events.map(ev => ({
489 metadata: ev.metadata,
490 error: ev.encodeError
491 }))
492 );
493 resolve();
494 return;
495 }
496
497 let userAgent = USER_AGENT;
498 let trimmedAddition = this._userAgentAddition.trim();
499 if (trimmedAddition) {
500 userAgent = `${USER_AGENT} ${trimmedAddition}`;
501 }
502
503 let start = Date.now();
504 req
505 .set("X-Honeycomb-Team", batch.writeKey)
506 .set(
507 "User-Agent",
508 userAgent
509 )
510 .type("json")
511 .send(encoded)
512 .end((err, res) => {
513 let end = Date.now();
514
515 if (err) {
516 this._responseCallback(
517 batch.events.map(ev => ({
518 // eslint-disable-next-line camelcase
519 status_code: ev.encodeError ? undefined : err.status,
520 duration: end - start,
521 metadata: ev.metadata,
522 error: ev.encodeError || err
523 }))
524 );
525 } else {
526 let response = JSON.parse(res.text);
527 let respIdx = 0;
528 this._responseCallback(
529 batch.events.map(ev => {
530 if (ev.encodeError) {
531 return {
532 duration: end - start,
533 metadata: ev.metadata,
534 error: ev.encodeError
535 };
536 } else {
537 let nextResponse = response[respIdx++];
538 return {
539 // eslint-disable-next-line camelcase
540 status_code: nextResponse.status,
541 duration: end - start,
542 metadata: ev.metadata,
543 error: nextResponse.err
544 };
545 }
546 })
547 );
548 }
549 // we resolve unconditionally to continue the iteration in eachSeries. errors will cause
550 // the event to be re-enqueued/dropped.
551 resolve();
552 });
553 })
554 );
555 })
556 .then(finishBatch)
557 .catch(finishBatch);
558 }
559
560 _shouldSendEvent(ev) {
561 let { sampleRate } = ev;
562 if (sampleRate <= 1) {
563 return true;
564 }
565 return this._randomFn() < 1 / sampleRate;
566 }
567
568 _ensureSendTimeout() {
569 if (this._sendTimeoutId === -1) {
570 this._sendTimeoutId = _global.setTimeout(
571 () => this._sendBatch(),
572 this._batchTimeTrigger
573 );
574 }
575 }
576
577 _clearSendTimeout() {
578 if (this._sendTimeoutId !== -1) {
579 _global.clearTimeout(this._sendTimeoutId);
580 this._sendTimeoutId = -1;
581 }
582 }
583}
584
585// Copyright 2016 Hound Technology, Inc. All rights reserved.
586// Use of this source code is governed by the Apache License 2.0
587// license that can be found in the LICENSE file.
588
589/**
590 * a simple function that offers the same interface
591 * for both Map and object key interation.
592 * @private
593 */
594function foreach(col, f) {
595 if (!col) {
596 return;
597 }
598 if (col instanceof Map) {
599 col.forEach(f);
600 } else {
601 Object.getOwnPropertyNames(col).forEach(k => f(col[k], k));
602 }
603}
604
605// Copyright 2016 Hound Technology, Inc. All rights reserved.
606
607/**
608 * Represents an individual event to send to Honeycomb.
609 * @class
610 */
611class Event {
612 /**
613 * @constructor
614 * private
615 */
616 constructor(libhoney, fields, dynFields) {
617 this.data = Object.create(null);
618 this.metadata = null;
619
620 /**
621 * The hostname for the Honeycomb API server to which to send this event. default:
622 * https://api.honeycomb.io/
623 *
624 * @type {string}
625 */
626 this.apiHost = "";
627 /**
628 * The Honeycomb authentication token for this event. Find your team write key at
629 * https://ui.honeycomb.io/account
630 *
631 * @type {string}
632 */
633 this.writeKey = "";
634 /**
635 * The name of the Honeycomb dataset to which to send this event.
636 *
637 * @type {string}
638 */
639 this.dataset = "";
640 /**
641 * The rate at which to sample this event.
642 *
643 * @type {number}
644 */
645 this.sampleRate = 1;
646
647 /**
648 * If set, specifies the timestamp associated with this event. If unset,
649 * defaults to Date.now();
650 *
651 * @type {Date}
652 */
653 this.timestamp = null;
654
655 foreach(fields, (v, k) => this.addField(k, v));
656 foreach(dynFields, (v, k) => this.addField(k, v()));
657
658 // stash this away for .send()
659 this._libhoney = libhoney;
660 }
661
662 /**
663 * adds a group of field->values to this event.
664 * @param {Object|Map} data field->value mapping.
665 * @returns {Event} this event.
666 * @example <caption>using an object</caption>
667 * builder.newEvent()
668 * .add ({
669 * responseTime_ms: 100,
670 * httpStatusCode: 200
671 * })
672 * .send();
673 * @example <caption>using an ES2015 map</caption>
674 * let map = new Map();
675 * map.set("responseTime_ms", 100);
676 * map.set("httpStatusCode", 200);
677 * let event = honey.newEvent();
678 * event.add (map);
679 * event.send();
680 */
681 add(data) {
682 foreach(data, (v, k) => this.addField(k, v));
683 return this;
684 }
685
686 /**
687 * adds a single field->value mapping to this event.
688 * @param {string} name
689 * @param {any} val
690 * @returns {Event} this event.
691 * @example
692 * builder.newEvent()
693 * .addField("responseTime_ms", 100)
694 * .send();
695 */
696 addField(name, val) {
697 if (val === undefined) {
698 this.data[name] = null;
699 return this;
700 }
701 this.data[name] = val;
702 return this;
703 }
704
705 /**
706 * attaches data to an event that is not transmitted to honeycomb, but instead is available when checking the send responses.
707 * @param {any} md
708 * @returns {Event} this event.
709 */
710 addMetadata(md) {
711 this.metadata = md;
712 return this;
713 }
714
715 /**
716 * Sends this event to honeycomb, sampling if necessary.
717 */
718 send() {
719 this._libhoney.sendEvent(this);
720 }
721
722 /**
723 * Dispatch an event to be sent to Honeycomb. Assumes sampling has already happened,
724 * and will send every event handed to it.
725 */
726 sendPresampled() {
727 this._libhoney.sendPresampledEvent(this);
728 }
729}
730
731// Copyright 2016 Hound Technology, Inc. All rights reserved.
732
733/**
734 * Allows piecemeal creation of events.
735 * @class
736 */
737class Builder {
738 /**
739 * @constructor
740 * @private
741 */
742 constructor(libhoney, fields, dynFields) {
743 this._libhoney = libhoney;
744 this._fields = Object.create(null);
745 this._dynFields = Object.create(null);
746
747 /**
748 * The hostname for the Honeycomb API server to which to send events created through this
749 * builder. default: https://api.honeycomb.io/
750 *
751 * @type {string}
752 */
753 this.apiHost = "";
754 /**
755 * The Honeycomb authentication token. If it is set on a libhoney instance it will be used as the
756 * default write key for all events. If absent, it must be explicitly set on a Builder or
757 * Event. Find your team write key at https://ui.honeycomb.io/account
758 *
759 * @type {string}
760 */
761 this.writeKey = "";
762 /**
763 * The name of the Honeycomb dataset to which to send these events. If it is specified during
764 * libhoney initialization, it will be used as the default dataset for all events. If absent,
765 * dataset must be explicitly set on a builder or event.
766 *
767 * @type {string}
768 */
769 this.dataset = "";
770 /**
771 * The rate at which to sample events. Default is 1, meaning no sampling. If you want to send one
772 * event out of every 250 times send() is called, you would specify 250 here.
773 *
774 * @type {number}
775 */
776 this.sampleRate = 1;
777
778 foreach(fields, (v, k) => this.addField(k, v));
779 foreach(dynFields, (v, k) => this.addDynamicField(k, v));
780 }
781
782 /**
783 * adds a group of field->values to the events created from this builder.
784 * @param {Object|Map<string, any>} data field->value mapping.
785 * @returns {Builder} this Builder instance.
786 * @example <caption>using an object</caption>
787 * var honey = new libhoney();
788 * var builder = honey.newBuilder();
789 * builder.add ({
790 * component: "web",
791 * depth: 200
792 * });
793 * @example <caption>using an ES2015 map</caption>
794 * let map = new Map();
795 * map.set("component", "web");
796 * map.set("depth", 200);
797 * builder.add (map);
798 */
799 add(data) {
800 foreach(data, (v, k) => this.addField(k, v));
801 return this;
802 }
803
804 /**
805 * adds a single field->value mapping to the events created from this builder.
806 * @param {string} name
807 * @param {any} val
808 * @returns {Builder} this Builder instance.
809 * @example
810 * builder.addField("component", "web");
811 */
812 addField(name, val) {
813 if (val === undefined) {
814 this._fields[name] = null;
815 return this;
816 }
817 this._fields[name] = val;
818 return this;
819 }
820
821 /**
822 * adds a single field->dynamic value function, which is invoked to supply values when events are created from this builder.
823 * @param {string} name the name of the field to add to events.
824 * @param {function(): any} fn the function called to generate the value for this field.
825 * @returns {Builder} this Builder instance.
826 * @example
827 * builder.addDynamicField("process_heapUsed", () => process.memoryUsage().heapUsed);
828 */
829 addDynamicField(name, fn) {
830 this._dynFields[name] = fn;
831 }
832
833 /**
834 * creates and sends an event, including all builder fields/dynFields, as well as anything in the optional data parameter.
835 * @param {Object|Map<string, any>} [data] field->value mapping to add to the event sent.
836 * @example <caption>empty sendNow</caption>
837 * builder.sendNow(); // sends just the data that has been added via add/addField/addDynamicField.
838 * @example <caption>adding data at send-time</caption>
839 * builder.sendNow({
840 * additionalField: value
841 * });
842 */
843 sendNow(data) {
844 let ev = this.newEvent();
845 ev.add(data);
846 ev.send();
847 }
848
849 /**
850 * creates and returns a new Event containing all fields/dynFields from this builder, that can be further fleshed out and sent on its own.
851 * @returns {Event} an Event instance
852 * @example <caption>adding data at send-time</caption>
853 * let ev = builder.newEvent();
854 * ev.addField("additionalField", value);
855 * ev.send();
856 */
857 newEvent() {
858 let ev = new Event(this._libhoney, this._fields, this._dynFields);
859 ev.apiHost = this.apiHost;
860 ev.writeKey = this.writeKey;
861 ev.dataset = this.dataset;
862 ev.sampleRate = this.sampleRate;
863 return ev;
864 }
865
866 /**
867 * creates and returns a clone of this builder, merged with fields and dynFields passed as arguments.
868 * @param {Object|Map<string, any>} fields a field->value mapping to merge into the new builder.
869 * @param {Object|Map<string, any>} dynFields a field->dynamic function mapping to merge into the new builder.
870 * @returns {Builder} a Builder instance
871 * @example <caption>no additional fields/dyn_field</caption>
872 * let anotherBuilder = builder.newBuilder();
873 * @example <caption>additional fields/dyn_field</caption>
874 * let anotherBuilder = builder.newBuilder({ requestId },
875 * {
876 * process_heapUsed: () => process.memoryUsage().heapUsed
877 * });
878 */
879 newBuilder(fields, dynFields) {
880 let b = new Builder(this._libhoney, this._fields, this._dynFields);
881
882 foreach(fields, (v, k) => b.addField(k, v));
883 foreach(dynFields, (v, k) => b.addDynamicField(k, v));
884
885 b.apiHost = this.apiHost;
886 b.writeKey = this.writeKey;
887 b.dataset = this.dataset;
888 b.sampleRate = this.sampleRate;
889
890 return b;
891 }
892}
893
894// Copyright 2016 Hound Technology, Inc. All rights reserved.
895
896const defaults = Object.freeze({
897 apiHost: "https://api.honeycomb.io/",
898
899 // http
900 proxy: undefined,
901
902 // sample rate of data. causes us to send 1/sample-rate of events
903 // i.e. `sampleRate: 10` means we only send 1/10th the events.
904 sampleRate: 1,
905
906 // transmission constructor, or a string to pick one of our builtin versions.
907 // we fall back to the base impl if worker or a custom implementation throws on init.
908 // string options available are:
909 // - "base": the default transmission implementation
910 // - "worker": a web-worker based transmission (not currently available, see https://github.com/honeycombio/libhoney-js/issues/22)
911 // - "mock": an implementation that accumulates all events sent
912 // - "writer": an implementation that logs to the console all events sent
913 // - "null": an implementation that does nothing
914 transmission: "base",
915
916 // batch triggers
917 batchSizeTrigger: 50, // we send a batch to the api when we have this many outstanding events
918 batchTimeTrigger: 100, // ... or after this many ms has passed.
919
920 // batches are sent serially (one event at a time), so we allow multiple concurrent batches
921 // to increase parallelism while sending.
922 maxConcurrentBatches: 10,
923
924 // the maximum number of pending events we allow in our to-be-batched-and-transmitted queue before dropping them.
925 pendingWorkCapacity: 10000,
926
927 // the maximum number of responses we enqueue before we begin dropping them.
928 maxResponseQueueSize: 1000,
929
930 // if this is set to true, all sending is disabled. useful for disabling libhoney when testing
931 disabled: false,
932
933 // If this is non-empty, append it to the end of the User-Agent header.
934 userAgentAddition: ""
935});
936
937/**
938 * libhoney aims to make it as easy as possible to create events and send them on into Honeycomb.
939 *
940 * See https://honeycomb.io/docs for background on this library.
941 * @class
942 */
943class Libhoney extends events.EventEmitter {
944 /**
945 * Constructs a libhoney context in order to configure default behavior,
946 * though each of its members (`apiHost`, `writeKey`, `dataset`, and
947 * `sampleRate`) may in fact be overridden on a specific Builder or Event.
948 *
949 * @param {Object} [opts] overrides for the defaults
950 * @param {string} [opts.apiHost=https://api.honeycomb.io] - Server host to receive Honeycomb events.
951 * @param {string} [opts.proxy] - The proxy to send events through.
952 * @param {string} opts.writeKey - Write key for your Honeycomb team. (Required)
953 * @param {string} opts.dataset - Name of the dataset that should contain this event. The dataset will be created for your team if it doesn't already exist.
954 * @param {number} [opts.sampleRate=1] - Sample rate of data. If set, causes us to send 1/sampleRate of events and drop the rest.
955 * @param {number} [opts.batchSizeTrigger=50] - We send a batch to the API when this many outstanding events exist in our event queue.
956 * @param {number} [opts.batchTimeTrigger=100] - We send a batch to the API after this many milliseconds have passed.
957 * @param {number} [opts.maxConcurrentBatches=10] - We process batches concurrently to increase parallelism while sending.
958 * @param {number} [opts.pendingWorkCapacity=10000] - The maximum number of pending events we allow to accumulate in our sending queue before dropping them.
959 * @param {number} [opts.maxResponseQueueSize=1000] - The maximum number of responses we enqueue before dropping them.
960 * @param {boolean} [opts.disabled=false] - Disable transmission of events to the specified `apiHost`, particularly useful for testing or development.
961 * @constructor
962 * @example
963 * import Libhoney from 'libhoney';
964 * let honey = new Libhoney({
965 * writeKey: "YOUR_WRITE_KEY",
966 * dataset: "honeycomb-js-example",
967 * // disabled: true // uncomment when testing or in development
968 * });
969 */
970 constructor(opts) {
971 super();
972 this._options = Object.assign(
973 { responseCallback: this._responseCallback.bind(this) },
974 defaults,
975 opts
976 );
977 this._transmission = getAndInitTransmission(
978 this._options.transmission,
979 this._options
980 );
981 this._usable = this._transmission !== null;
982 this._builder = new Builder(this);
983
984 this._builder.apiHost = this._options.apiHost;
985 this._builder.writeKey = this._options.writeKey;
986 this._builder.dataset = this._options.dataset;
987 this._builder.sampleRate = this._options.sampleRate;
988
989 this._responseQueue = [];
990 }
991
992 _responseCallback(responses) {
993 let queue = this._responseQueue;
994 if (queue.length < this._options.maxResponseQueueSize) {
995 this._responseQueue = this._responseQueue.concat(responses);
996 }
997 this.emit("response", this._responseQueue);
998 }
999
1000 /**
1001 * The transmission implementation in use for this libhoney instance. Useful when mocking libhoney (specify
1002 * "mock" for options.transmission, and use this field to get at the list of events sent through libhoney.)
1003 */
1004 get transmission() {
1005 return this._transmission;
1006 }
1007
1008 /**
1009 * The hostname for the Honeycomb API server to which to send events created through this libhoney
1010 * instance. default: https://api.honeycomb.io/
1011 *
1012 * @type {string}
1013 */
1014 set apiHost(v) {
1015 this._builder.apiHost = v;
1016 }
1017 /**
1018 * The hostname for the Honeycomb API server to which to send events created through this libhoney
1019 * instance. default: https://api.honeycomb.io/
1020 *
1021 * @type {string}
1022 */
1023 get apiHost() {
1024 return this._builder.apiHost;
1025 }
1026
1027 /**
1028 * The Honeycomb authentication token. If it is set on a libhoney instance it will be used as the
1029 * default write key for all events. If absent, it must be explicitly set on a Builder or
1030 * Event. Find your team write key at https://ui.honeycomb.io/account
1031 *
1032 * @type {string}
1033 */
1034 set writeKey(v) {
1035 this._builder.writeKey = v;
1036 }
1037 /**
1038 * The Honeycomb authentication token. If it is set on a libhoney instance it will be used as the
1039 * default write key for all events. If absent, it must be explicitly set on a Builder or
1040 * Event. Find your team write key at https://ui.honeycomb.io/account
1041 *
1042 * @type {string}
1043 */
1044 get writeKey() {
1045 return this._builder.writeKey;
1046 }
1047
1048 /**
1049 * The name of the Honeycomb dataset to which to send events through this libhoney instance. If
1050 * it is specified during libhoney initialization, it will be used as the default dataset for all
1051 * events. If absent, dataset must be explicitly set on a builder or event.
1052 *
1053 * @type {string}
1054 */
1055 set dataset(v) {
1056 this._builder.dataset = v;
1057 }
1058 /**
1059 * The name of the Honeycomb dataset to which to send these events through this libhoney instance.
1060 * If it is specified during libhoney initialization, it will be used as the default dataset for
1061 * all events. If absent, dataset must be explicitly set on a builder or event.
1062 *
1063 * @type {string}
1064 */
1065 get dataset() {
1066 return this._builder.dataset;
1067 }
1068
1069 /**
1070 * The rate at which to sample events. Default is 1, meaning no sampling. If you want to send one
1071 * event out of every 250 times send() is called, you would specify 250 here.
1072 *
1073 * @type {number}
1074 */
1075 set sampleRate(v) {
1076 this._builder.sampleRate = v;
1077 }
1078 /**
1079 * The rate at which to sample events. Default is 1, meaning no sampling. If you want to send one
1080 * event out of every 250 times send() is called, you would specify 250 here.
1081 *
1082 * @type {number}
1083 */
1084 get sampleRate() {
1085 return this._builder.sampleRate;
1086 }
1087
1088 /**
1089 * sendEvent takes events of the following form:
1090 *
1091 * {
1092 * data: a JSON-serializable object, keys become colums in Honeycomb
1093 * timestamp [optional]: time for this event, defaults to now()
1094 * writeKey [optional]: your team's write key. overrides the libhoney instance's value.
1095 * dataset [optional]: the data set name. overrides the libhoney instance's value.
1096 * sampleRate [optional]: cause us to send 1 out of sampleRate events. overrides the libhoney instance's value.
1097 * }
1098 *
1099 * Sampling is done based on the supplied sampleRate, so events passed to this method might not
1100 * actually be sent to Honeycomb.
1101 * @private
1102 */
1103 sendEvent(event) {
1104 let transmitEvent = this.validateEvent(event);
1105 if (!transmitEvent) {
1106 return;
1107 }
1108
1109 this._transmission.sendEvent(transmitEvent);
1110 }
1111
1112 /**
1113 * sendPresampledEvent takes events of the following form:
1114 *
1115 * {
1116 * data: a JSON-serializable object, keys become colums in Honeycomb
1117 * timestamp [optional]: time for this event, defaults to now()
1118 * writeKey [optional]: your team's write key. overrides the libhoney instance's value.
1119 * dataset [optional]: the data set name. overrides the libhoney instance's value.
1120 * sampleRate: the rate this event has already been sampled.
1121 * }
1122 *
1123 * Sampling is presumed to have already been done (at the supplied sampledRate), so all events passed to this method
1124 * are sent to Honeycomb.
1125 * @private
1126 */
1127 sendPresampledEvent(event) {
1128 let transmitEvent = this.validateEvent(event);
1129 if (!transmitEvent) {
1130 return;
1131 }
1132
1133 this._transmission.sendPresampledEvent(transmitEvent);
1134 }
1135
1136 /**
1137 * validateEvent takes an event and validates its structure and contents.
1138 *
1139 * @returns {Object} the validated libhoney Event. May return undefined if
1140 * the event was invalid in some way or unable to be sent.
1141 * @private
1142 */
1143 validateEvent(event) {
1144 if (!this._usable) return null;
1145
1146 let timestamp = event.timestamp || Date.now();
1147 if (typeof timestamp === "string" || typeof timestamp === "number")
1148 timestamp = new Date(timestamp);
1149
1150 if (typeof event.data !== "object" || event.data === null) {
1151 console.error(".data must be an object");
1152 return null;
1153 }
1154 let postData;
1155 try {
1156 postData = JSON.stringify(event.data);
1157 } catch (e) {
1158 console.error("error converting event data to JSON: " + e);
1159 return null;
1160 }
1161
1162 let apiHost = event.apiHost;
1163 if (typeof apiHost !== "string" || apiHost === "") {
1164 console.error(".apiHost must be a non-empty string");
1165 return null;
1166 }
1167
1168 let writeKey = event.writeKey;
1169 if (typeof writeKey !== "string" || writeKey === "") {
1170 console.error(".writeKey must be a non-empty string");
1171 return null;
1172 }
1173
1174 let dataset = event.dataset;
1175 if (typeof dataset !== "string" || dataset === "") {
1176 console.error(".dataset must be a non-empty string");
1177 return null;
1178 }
1179
1180 let sampleRate = event.sampleRate;
1181 if (typeof sampleRate !== "number") {
1182 console.error(".sampleRate must be a number");
1183 return null;
1184 }
1185
1186 let metadata = event.metadata;
1187 return new ValidatedEvent({
1188 timestamp,
1189 apiHost,
1190 postData,
1191 writeKey,
1192 dataset,
1193 sampleRate,
1194 metadata
1195 });
1196 }
1197
1198 /**
1199 * adds a group of field->values to the global Builder.
1200 * @param {Object|Map<string, any>} data field->value mapping.
1201 * @returns {Libhoney} this libhoney instance.
1202 * @example <caption>using an object</caption>
1203 * honey.add ({
1204 * buildID: "a6cc38a1",
1205 * env: "staging"
1206 * });
1207 * @example <caption>using an ES2015 map</caption>
1208 * let map = new Map();
1209 * map.set("build_id", "a6cc38a1");
1210 * map.set("env", "staging");
1211 * honey.add (map);
1212 */
1213 add(data) {
1214 this._builder.add(data);
1215 return this;
1216 }
1217
1218 /**
1219 * adds a single field->value mapping to the global Builder.
1220 * @param {string} name name of field to add.
1221 * @param {any} val value of field to add.
1222 * @returns {Libhoney} this libhoney instance.
1223 * @example
1224 * honey.addField("build_id", "a6cc38a1");
1225 */
1226 addField(name, val) {
1227 this._builder.addField(name, val);
1228 return this;
1229 }
1230
1231 /**
1232 * adds a single field->dynamic value function to the global Builder.
1233 * @param {string} name name of field to add.
1234 * @param {function(): any} fn function that will be called to generate the value whenever an event is created.
1235 * @returns {Libhoney} this libhoney instance.
1236 * @example
1237 * honey.addDynamicField("process_heapUsed", () => process.memoryUsage().heapUsed);
1238 */
1239 addDynamicField(name, fn) {
1240 this._builder.addDynamicField(name, fn);
1241 return this;
1242 }
1243
1244 /**
1245 * creates and sends an event, including all global builder fields/dynFields, as well as anything in the optional data parameter.
1246 * @param {Object|Map<string, any>} data field->value mapping.
1247 * @example <caption>using an object</caption>
1248 * honey.sendNow ({
1249 * responseTime_ms: 100,
1250 * httpStatusCode: 200
1251 * });
1252 * @example <caption>using an ES2015 map</caption>
1253 * let map = new Map();
1254 * map.set("responseTime_ms", 100);
1255 * map.set("httpStatusCode", 200);
1256 * honey.sendNow (map);
1257 */
1258 sendNow(data) {
1259 return this._builder.sendNow(data);
1260 }
1261
1262 /**
1263 * creates and returns a new Event containing all fields/dynFields from the global Builder, that can be further fleshed out and sent on its own.
1264 * @returns {Event} an Event instance
1265 * @example <caption>adding data at send-time</caption>
1266 * let ev = honey.newEvent();
1267 * ev.addField("additionalField", value);
1268 * ev.send();
1269 */
1270 newEvent() {
1271 return this._builder.newEvent();
1272 }
1273
1274 /**
1275 * creates and returns a clone of the global Builder, merged with fields and dynFields passed as arguments.
1276 * @param {Object|Map<string, any>} fields a field->value mapping to merge into the new builder.
1277 * @param {Object|Map<string, any>} dynFields a field->dynamic function mapping to merge into the new builder.
1278 * @returns {Builder} a Builder instance
1279 * @example <caption>no additional fields/dyn_field</caption>
1280 * let builder = honey.newBuilder();
1281 * @example <caption>additional fields/dyn_field</caption>
1282 * let builder = honey.newBuilder({ requestId },
1283 * {
1284 * process_heapUsed: () => process.memoryUsage().heapUsed
1285 * });
1286 */
1287 newBuilder(fields, dynFields) {
1288 return this._builder.newBuilder(fields, dynFields);
1289 }
1290
1291 /**
1292 * Allows you to easily wait for everything to be sent to Honeycomb (and for responses to come back for
1293 * events). Also initializes a transmission instance for libhoney to use, so any events sent
1294 * after a call to flush will not be waited on.
1295 * @returns {Promise} a promise that will resolve when all currently enqueued events/batches are sent.
1296 */
1297 flush() {
1298 const transmission = this._transmission;
1299
1300 this._transmission = getAndInitTransmission(
1301 this._options.transmission,
1302 this._options
1303 );
1304
1305 return transmission.flush();
1306 }
1307}
1308
1309const getTransmissionClass = transmissionClassName => {
1310 switch (transmissionClassName) {
1311 case "base":
1312 return Transmission;
1313 case "mock":
1314 return MockTransmission;
1315 case "null":
1316 return NullTransmission;
1317 case "worker":
1318 console.warn(
1319 "worker implementation not ready yet. using base implementation"
1320 );
1321 return Transmission;
1322 case "writer":
1323 return WriterTransmission;
1324 default:
1325 throw new Error(
1326 `unknown transmission implementation "${transmissionClassName}".`
1327 );
1328 }
1329};
1330
1331function getAndInitTransmission(transmission, options) {
1332 if (options.disabled) {
1333 return null;
1334 }
1335
1336 if (typeof transmission === "string") {
1337 const transmissionClass = getTransmissionClass(transmission);
1338 return new transmissionClass(options);
1339 } else if (typeof transmission !== "function") {
1340 throw new Error(
1341 ".transmission must be one of 'base'/'worker'/'mock'/'writer'/'null' or a constructor."
1342 );
1343 }
1344
1345 try {
1346 return new transmission(options);
1347 } catch (initialisationError) {
1348 if (transmission === Transmission) {
1349 throw new Error(
1350 "unable to initialize base transmission implementation.",
1351 initialisationError
1352 );
1353 }
1354
1355 console.warn(
1356 "failed to initialize transmission, falling back to base implementation."
1357 );
1358 try {
1359 return new Transmission(options);
1360 } catch (fallbackInitialisationError) {
1361 throw new Error(
1362 "unable to initialize base transmission implementation.",
1363 fallbackInitialisationError
1364 );
1365 }
1366 }
1367}
1368
1369module.exports = Libhoney;