UNPKG

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