UNPKG

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