UNPKG

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