UNPKG

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