forked from newrelic/node-newrelic
-
Notifications
You must be signed in to change notification settings - Fork 0
/
api.js
1905 lines (1689 loc) · 66.1 KB
/
api.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
'use strict'
const util = require('util')
const logger = require('./lib/logger').child({ component: 'api' })
const recordWeb = require('./lib/metrics/recorders/http')
const recordBackground = require('./lib/metrics/recorders/other')
const customRecorder = require('./lib/metrics/recorders/custom')
const hashes = require('./lib/util/hashes')
const properties = require('./lib/util/properties')
const stringify = require('json-stringify-safe')
const shimmer = require('./lib/shimmer')
const isValidType = require('./lib/util/attribute-types')
const TransactionShim = require('./lib/shim/transaction-shim')
const TransactionHandle = require('./lib/transaction/handle')
const AwsLambda = require('./lib/serverless/aws-lambda')
const applicationLogging = require('./lib/util/application-logging')
const {
assignCLMSymbol,
addCLMAttributes: maybeAddCLMAttributes
} = require('./lib/util/code-level-metrics')
const LlmFeedbackMessage = require('./lib/llm-events/feedback-message')
const ATTR_DEST = require('./lib/config/attribute-filter').DESTINATIONS
const MODULE_TYPE = require('./lib/instrumentation-descriptor').TYPES
const NAMES = require('./lib/metrics/names')
const obfuscate = require('./lib/util/sql/obfuscate')
const { DESTINATIONS } = require('./lib/config/attribute-filter')
const parse = require('module-details-from-path')
const { isSimpleObject } = require('./lib/util/objects')
/*
*
* CONSTANTS
*
*/
const RUM_STUB = 'window.NREUM||(NREUM={});NREUM.info = %s; %s'
const RUM_STUB_SHELL = `<script type='text/javascript'>${RUM_STUB}</script>`
const RUM_STUB_SHELL_WITH_NONCE_PARAM = `<script type='text/javascript' %s>${RUM_STUB}</script>`
// these messages are used in the _gracefail() method below in getBrowserTimingHeader
const RUM_ISSUES = [
'NREUM: no browser monitoring headers generated; disabled',
'NREUM: transaction ignored while generating browser monitoring headers',
'NREUM: config.browser_monitoring missing, something is probably wrong',
'NREUM: browser_monitoring headers need a transaction name',
'NREUM: browser_monitoring requires valid application_id',
'NREUM: browser_monitoring requires valid browser_key',
'NREUM: browser_monitoring requires js_agent_loader script',
'NREUM: browser_monitoring disabled by browser_monitoring.loader config'
]
// Can't overwrite internal parameters or all heck will break loose.
const CUSTOM_DENYLIST = new Set(['nr_flatten_leading'])
const CUSTOM_EVENT_TYPE_REGEX = /^[a-zA-Z0-9:_ ]+$/
/**
* The exported New Relic API. This contains all of the functions meant to be
* used by New Relic customers.
*
* You do not need to directly instantiate this class, as an instance of this is
* the return from `require('newrelic')`.
*
* @param {object} agent Instantiation of lib/agent.js
* @class
*/
function API(agent) {
this.agent = agent
this.shim = new TransactionShim(agent, 'NewRelicAPI')
this.awsLambda = new AwsLambda(agent)
}
/**
* Give the current transaction a custom name. Overrides any New Relic naming
* rules set in configuration or from New Relic's servers.
*
* IMPORTANT: this function must be called when a transaction is active. New
* Relic transactions are tied to web requests, so this method may be called
* from within HTTP or HTTPS listener functions, Express routes, or other
* contexts where a web request or response object are in scope.
*
* @param {string} name The name you want to give the web request in the New
* Relic UI. Will be prefixed with 'Custom/' when sent.
* @returns {void}
*/
API.prototype.setTransactionName = function setTransactionName(name) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/setTransactionName'
)
metric.incrementCallCount()
const transaction = this.agent.tracer.getTransaction()
if (!transaction) {
return logger.warn("No transaction found when setting name to '%s'.", name)
}
if (!name) {
if (transaction && transaction.url) {
logger.error('Must include name in setTransactionName call for URL %s.', transaction.url)
} else {
logger.error('Must include name in setTransactionName call.')
}
return
}
logger.trace('Setting transaction %s name to %s', transaction.id, name)
transaction.forceName = NAMES.CUSTOM + '/' + name
}
/**
* This method returns an object with the following methods:
* - end: end the transaction that was active when `API#getTransaction`
* was called.
*
* - ignore: set the transaction that was active when
* `API#getTransaction` was called to be ignored.
*
* @returns {TransactionHandle} The transaction object with the `end` and
* `ignore` methods on it.
*/
API.prototype.getTransaction = function getTransaction() {
const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/getTransaction')
metric.incrementCallCount()
const transaction = this.agent.tracer.getTransaction()
if (!transaction) {
logger.debug('No transaction found when calling API#getTransaction')
return new TransactionHandle.Stub()
}
transaction.handledExternally = true
return new TransactionHandle(transaction, this.agent.metrics)
}
/**
* This method returns an object with the following keys/data:
* - `trace.id`: The current trace ID
* - `span.id`: The current span ID
* - `entity.name`: The application name specified in the connect request as
* app_name. If multiple application names are specified this will only be
* the first name
* - `entity.type`: The string "SERVICE"
* - `entity.guid`: The entity ID returned in the connect reply as entity_guid
* - `hostname`: The hostname as specified in the connect request as
* utilization.full_hostname. If utilization.full_hostname is null or empty,
* this will be the hostname specified in the connect request as host.
*
* @param {boolean} omitSupportability Whether or not to log the supportability metric, true means skip
* @returns {object} The LinkingMetadata object with the data above
*/
API.prototype.getLinkingMetadata = function getLinkingMetadata(omitSupportability) {
if (omitSupportability !== true) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/getLinkingMetadata'
)
metric.incrementCallCount()
}
return this.agent.getLinkingMetadata()
}
/**
* Specify the `Dispatcher` and `Dispatcher Version` environment values.
* A dispatcher is typically the service responsible for brokering
* the request with the process responsible for responding to the
* request. For example Node's `http` module would be the dispatcher
* for incoming HTTP requests.
*
* @param {string} name The string you would like to report to New Relic
* as the dispatcher.
* @param {string} [version] The dispatcher version you would like to
* report to New Relic
*/
API.prototype.setDispatcher = function setDispatcher(name, version) {
const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/setDispatcher')
metric.incrementCallCount()
if (!name || typeof name !== 'string') {
logger.error('setDispatcher must be called with a name, and name must be a string.')
return
}
// No objects allowed.
if (version && typeof version !== 'object') {
version = String(version)
} else {
logger.info('setDispatcher was called with an object as the version parameter')
version = null
}
this.agent.environment.setDispatcher(name, version, true)
}
/**
* Give the current transaction a name based on your own idea of what
* constitutes a controller in your Node application. Also allows you to
* optionally specify the action being invoked on the controller. If the action
* is omitted, then the API will default to using the HTTP method used in the
* request (e.g. GET, POST, DELETE). Overrides any New Relic naming rules set
* in configuration or from New Relic's servers.
*
* IMPORTANT: this function must be called when a transaction is active. New
* Relic transactions are tied to web requests, so this method may be called
* from within HTTP or HTTPS listener functions, Express routes, or other
* contexts where a web request or response object are in scope.
*
* @param {string} name The name you want to give the controller in the New
* Relic UI. Will be prefixed with 'Controller/' when
* sent.
* @param {string} action The action being invoked on the controller. Defaults
* to the HTTP method used for the request.
* @returns {void}
*/
API.prototype.setControllerName = function setControllerName(name, action) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/setControllerName'
)
metric.incrementCallCount()
const transaction = this.agent.tracer.getTransaction()
if (!transaction) {
return logger.warn('No transaction found when setting controller to %s.', name)
}
if (!name) {
if (transaction && transaction.url) {
logger.error('Must include name in setControllerName call for URL %s.', transaction.url)
} else {
logger.error('Must include name in setControllerName call.')
}
return
}
action = action || transaction.verb || 'GET'
transaction.forceName = NAMES.CONTROLLER + '/' + name + '/' + action
}
/**
* Add a custom attribute to the current transaction and span. Some attributes are
* reserved (see CUSTOM_DENYLIST for the current, very short list), and
* as with most API methods, this must be called in the context of an
* active transaction. Most recently set value wins.
*
* @param {string} key The key you want displayed in the RPM UI.
* @param {string} value The value you want displayed. Must be serializable.
* @returns {false|undefined} Returns false when disabled/errored, otherwise undefined
*/
API.prototype.addCustomAttribute = function addCustomAttribute(key, value) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/addCustomAttribute'
)
metric.incrementCallCount()
// If high security mode is on, custom attributes are disabled.
if (this.agent.config.high_security) {
logger.warnOnce('Custom attributes', 'Custom attributes are disabled by high security mode.')
return false
} else if (!this.agent.config.api.custom_attributes_enabled) {
logger.debug('Config.api.custom_attributes_enabled set to false, not collecting value')
return false
}
const transaction = this.agent.tracer.getTransaction()
if (!transaction) {
logger.warn('No transaction found for custom attributes.')
return false
}
const trace = transaction.trace
if (!trace.custom) {
logger.warn('Could not add attribute %s to nonexistent custom attributes.', key)
return false
}
if (CUSTOM_DENYLIST.has(key)) {
logger.warn('Not overwriting value of NR-only attribute %s.', key)
return false
}
trace.addCustomAttribute(key, value)
const spanContext = this.agent.tracer.getSpanContext()
if (!spanContext) {
logger.debug('No span found for custom attributes.')
// success/failure is ambiguous here. since at least 1 attempt tried, not returning false
return
}
spanContext.addCustomAttribute(key, value, spanContext.ATTRIBUTE_PRIORITY.LOW)
}
/**
* Adds all custom attributes in an object to the current transaction and span.
*
* See documentation for newrelic.addCustomAttribute for more information on
* setting custom attributes.
*
* An example of setting a custom attribute object:
*
* newrelic.addCustomAttributes({test: 'value', test2: 'value2'});
*
* @param {object} [atts] Attribute object
* @param {string} [atts.KEY] The name you want displayed in the RPM UI.
* @param {string} [atts.KEY.VALUE] The value you want displayed. Must be serializable.
*/
API.prototype.addCustomAttributes = function addCustomAttributes(atts) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/addCustomAttributes'
)
metric.incrementCallCount()
for (const key in atts) {
if (!properties.hasOwn(atts, key)) {
continue
}
this.addCustomAttribute(key, atts[key])
}
}
/**
* Add custom span attributes in an object to the current segment/span.
*
* See documentation for newrelic.addCustomSpanAttribute for more information.
*
* An example of setting a custom span attribute:
*
* newrelic.addCustomSpanAttribute({test: 'value', test2: 'value2'})
*
* @param {object} [atts] Attribute object
* @param {string} [atts.KEY] The name you want displayed in the RPM UI.API.
* @param {string} [atts.KEY.VALUE] The value you want displayed. Must be serializable.
*/
API.prototype.addCustomSpanAttributes = function addCustomSpanAttributes(atts) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/addCustomSpanAttributes'
)
metric.incrementCallCount()
for (const key in atts) {
if (properties.hasOwn(atts, key)) {
this.addCustomSpanAttribute(key, atts[key])
}
}
}
/**
* Add a custom span attribute to the current transaction. Some attributes
* are reserved (see CUSTOM_DENYLIST for the current, very short list), and
* as with most API methods, this must be called in the context of an
* active segment/span. Most recently set value wins.
*
* @param {string} key The key you want displayed in the RPM UI.
* @param {string} value The value you want displayed. Must be serializable.
* @returns {false|undefined} Returns false when disabled/errored, otherwise undefined
*/
API.prototype.addCustomSpanAttribute = function addCustomSpanAttribute(key, value) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/addCustomSpanAttribute'
)
metric.incrementCallCount()
// If high security mode is on, custom attributes are disabled.
if (this.agent.config.high_security) {
logger.warnOnce(
'Custom span attributes',
'Custom span attributes are disabled by high security mode.'
)
return false
} else if (!this.agent.config.api.custom_attributes_enabled) {
logger.debug('Config.api.custom_attributes_enabled set to false, not collecting value')
return false
}
const spanContext = this.agent.tracer.getSpanContext()
if (!spanContext) {
logger.debug('Could not add attribute %s. No available span.', key)
return false
}
if (CUSTOM_DENYLIST.has(key)) {
logger.warn('Not overwriting value of NR-only attribute %s.', key)
return false
}
spanContext.addCustomAttribute(key, value)
}
/**
* Send errors to New Relic that you've already handled yourself. Should be an
* `Error` or one of its subtypes, but the API will handle strings and objects
* that have an attached `.message` or `.stack` property.
*
* An example of using this function is
*
* try {
* performSomeTask();
* } catch (err) {
* newrelic.noticeError(
* err,
* {extraInformation: "error already handled in the application"},
* true
* );
* }
*
* NOTE: Errors that are recorded using this method do _not_ obey the
* `ignore_status_codes` configuration.
*
* @param {Error} error
* The error to be traced.
* @param {object} [customAttributes]
* Optional. Any custom attributes to be displayed in the New Relic UI.
* @param {boolean} expected
* Optional. False by default. True if the error is expected, meaning it should be collected
* for error events and traces, but should not impact error rate.
* @returns {false|undefined} Returns false when disabled/errored, otherwise undefined
*/
API.prototype.noticeError = function noticeError(error, customAttributes, expected = false) {
const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/noticeError')
metric.incrementCallCount()
// let users skip the custom attributes if they want
if (customAttributes && typeof customAttributes === 'boolean') {
expected = customAttributes
customAttributes = null
}
if (!this.agent.config.api.notice_error_enabled) {
logger.debug('Config.api.notice_error_enabled set to false, not collecting error')
return false
}
// If high security mode is on or custom attributes are disabled,
// noticeError does not collect custom attributes.
if (this.agent.config.high_security) {
logger.debug('Passing custom attributes to notice error API is disabled in high security mode.')
} else if (!this.agent.config.api.custom_attributes_enabled) {
logger.debug(
'Config.api.custom_attributes_enabled set to false, ' + 'ignoring custom error attributes.'
)
}
if (typeof error === 'string') {
error = new Error(error)
}
// Filter all object type valued attributes out
let filteredAttributes = customAttributes
if (customAttributes) {
filteredAttributes = _filterAttributes(customAttributes, 'noticeError')
}
const transaction = this.agent.tracer.getTransaction()
this.agent.errors.addUserError(transaction, error, filteredAttributes, expected)
}
/**
* Sends an application log message to New Relic. The agent already
* automatically does this for some instrumented logging libraries,
* but in case you are using another logging method that is not
* already instrumented by the agent, you can use this function
* instead.
*
* If application log forwarding is disabled in the agent
* configuration, this function does nothing.
*
* An example of using this function is
*
* newrelic.recordLogEvent({
* message: 'cannot find file',
* level: 'ERROR',
* error: new SystemError('missing.txt')
* })
*
* @param {object} logEvent The log event object to send. Any
* attributes besides `message`, `level`, `timestamp`, and `error` are
* recorded unchanged. The `logEvent` object itself will be mutated by
* this function.
* @param {string} logEvent.message The log message.
* @param {string} logEvent.level The log level severity. If this key is
* missing, it will default to UNKNOWN
* @param {number} logEvent.timestamp ECMAScript epoch number denoting the
* time that this log message was produced. If this key is missing,
* it will default to the output of `Date.now()`.
* @param {Error} logEvent.error Error associated to this log event. Ignored if missing.
*/
API.prototype.recordLogEvent = function recordLogEvent(logEvent = {}) {
const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/recordLogEvent')
metric.incrementCallCount()
if (!applicationLogging.isLogForwardingEnabled(this.agent.config, this.agent)) {
logger.warnOnce(
'Record logs',
'Application log forwarding disabled, method API#recordLogEvent will not record messages'
)
return
}
// If they don't pass a logEvent object, or it doesn't have the
// required `message` key, bail out.
if (typeof logEvent !== 'object' || logEvent.message === undefined) {
logger.warn(
'recordLogEvent requires an object with a `message` attribute for its single argument, got %s (%s)',
stringify(logEvent),
typeof logEvent
)
return
}
logEvent.message = applicationLogging.truncate(logEvent.message)
if (!logEvent.level) {
logger.debug('no log level set, setting it to UNKNOWN')
logEvent.level = 'UNKNOWN'
}
if (typeof logEvent.timestamp !== 'number') {
logger.debug('no timestamp set, setting it to `Date.now()`')
logEvent.timestamp = Date.now()
}
if (logEvent.error) {
logEvent['error.message'] = applicationLogging.truncate(logEvent.error.message)
logEvent['error.stack'] = applicationLogging.truncate(logEvent.error.stack)
logEvent['error.class'] =
logEvent.error.name === 'Error' ? logEvent.error.constructor.name : logEvent.error.name
delete logEvent.error
}
if (applicationLogging.isMetricsEnabled(this.agent.config)) {
applicationLogging.incrementLoggingLinesMetrics(logEvent.level, this.agent.metrics)
}
const metadata = this.agent.getLinkingMetadata()
this.agent.logs.add(Object.assign({}, logEvent, metadata))
}
/**
* If the URL for a transaction matches the provided pattern, name the
* transaction with the provided name. If there are capture groups in the
* pattern (which is a standard JavaScript regular expression, and can be
* passed as either a RegExp or a string), then the substring matches ($1, $2,
* etc.) are replaced in the name string. BE CAREFUL WHEN USING SUBSTITUTION.
* If the replacement substrings are highly variable (i.e. are identifiers,
* GUIDs, or timestamps), the rule will generate too many metrics and
* potentially get your application blocked by New Relic.
*
* An example of a good rule with replacements:
*
* newrelic.addNamingRule('^/storefront/(v[1-5])/(item|category|tag)',
* 'CommerceAPI/$1/$2')
*
* An example of a bad rule with replacements:
*
* newrelic.addNamingRule('^/item/([0-9a-f]+)', 'Item/$1')
*
* Keep in mind that the original URL and any query parameters will be sent
* along with the request, so slow transactions will still be identifiable.
*
* Naming rules can not be removed once added. They can also be added via the
* agent's configuration. See configuration documentation for details.
*
* @param {RegExp} pattern The pattern to rename (with capture groups).
* @param {string} name The name to use for the transaction.
* @returns {void}
*/
API.prototype.addNamingRule = function addNamingRule(pattern, name) {
const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/addNamingRule')
metric.incrementCallCount()
if (!name) {
return logger.error('Simple naming rules require a replacement name.')
}
this.agent.userNormalizer.addSimple(pattern, '/' + name)
}
/**
* If the URL for a transaction matches the provided pattern, ignore the
* transaction attached to that URL. Useful for filtering socket.io connections
* and other long-polling requests out of your agents to keep them from
* distorting an app's apdex or mean response time. Pattern may be a (standard
* JavaScript) RegExp or a string.
*
* Example:
*
* newrelic.addIgnoringRule('^/socket\\.io/')
*
* @param {RegExp} pattern The pattern to ignore.
* @returns {void}
*/
API.prototype.addIgnoringRule = function addIgnoringRule(pattern) {
const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/addIgnoringRule')
metric.incrementCallCount()
if (!pattern) {
return logger.error('Must include a URL pattern to ignore.')
}
this.agent.userNormalizer.addSimple(pattern, null)
}
/**
* Gracefully fail.
*
* Output an HTML comment and log a warning the comment is meant to be
* innocuous to the end user.
*
* @private
* @see RUM_ISSUES
* @param {number} errorCode Error code from `RUM_ISSUES`.
* @param {boolean} [quiet] Be quiet about this failure.
* @returns {string} HTML comment for debugging purposes with specific error code
*/
function _gracefail(errorCode, quiet) {
if (quiet) {
logger.debug(RUM_ISSUES[errorCode])
} else {
logger.warn(RUM_ISSUES[errorCode])
}
return '<!-- NREUM: (' + errorCode + ') -->'
}
/**
* Function for generating a fully formed RUM header based on configuration options
*
* @private
* @param {object} options Configuration options for RUM
* @param {string} [options.nonce] Nonce to inject into `<script>` header.
* @param {boolean} [options.hasToRemoveScriptWrapper] Used to import agent script without `<script>` tag wrapper.
* @param {string} metadata Stringified representation of rumHash metadata
* @param {string} loader Agent Loader script
* @returns {string} fully formed RUM header
*/
function _generateRUMHeader(options = {}, metadata, loader) {
const formatArgs = []
if (options.hasToRemoveScriptWrapper) {
formatArgs.push(RUM_STUB)
} else if (options.nonce) {
formatArgs.push(RUM_STUB_SHELL_WITH_NONCE_PARAM, `nonce="${options.nonce}"`)
} else {
formatArgs.push(RUM_STUB_SHELL)
}
formatArgs.push(metadata, loader)
return util.format(...formatArgs)
}
/**
* Helper method for determining if we have the minimum required
* information to generate our Browser Agent script tag
*
* @private
* @param {object} config agent configuration settings
* @param {Transaction} transaction the active transaction or null
* @param {boolean} allowTransactionlessInjection whether or not to allow the Browser Agent to be injected when there is no active transaction
* @returns {{ isValidConfig: boolean, failureIdx: number, quietMode: boolean }} object containing validation results
*/
function validateBrowserMonitoring(config, transaction, allowTransactionlessInjection) {
/*
* config.browser_monitoring should always exist, but we don't want the agent
* to bail here if something goes wrong
*/
if (!config.browser_monitoring) {
return { isValidConfig: false, failureIdx: 2 }
}
/*
* Can control header generation with configuration this setting is only
* available in the newrelic.js config file, it is not ever set by the
* server.
*/
if (!config.browser_monitoring.enable) {
// It has been disabled by the user; no need to warn them about their own
// settings so fail quietly and gracefully.
return { isValidConfig: false, failureIdx: 0, quietMode: true }
}
/*
* This is only going to work if the agent has successfully handshaked with
* the collector. If the networks is bad, or there is no license key set in
* newrelic.js, there will be no application_id set. We bail instead of
* outputting null/undefined configuration values.
*/
if (!config.application_id) {
return { isValidConfig: false, failureIdx: 4 }
}
/*
* If there is no browser_key, the server has likely decided to disable
* browser monitoring.
*/
if (!config.browser_monitoring.browser_key) {
return { isValidConfig: false, failureIdx: 5 }
}
/*
* If there is no agent_loader script, there is no point
* in setting the rum data
*/
if (!config.browser_monitoring.js_agent_loader) {
return { isValidConfig: false, failureIdx: 6 }
}
/*
* If rum is enabled, but then later disabled on the server,
* this is the only parameter that gets updated.
*
* This condition should only be met if rum is disabled during
* the lifetime of an application, and it should be picked up
* on the next ForceRestart by the collector.
*/
if (config.browser_monitoring.loader === 'none') {
return { isValidConfig: false, failureIdx: 7 }
}
if (!allowTransactionlessInjection && !transaction) {
return { isValidConfig: false, failureIdx: 1 }
}
return { isValidConfig: true }
}
/**
* Get the script header necessary for Browser Monitoring
* This script must be manually injected into your templates, as high as possible
* in the header, but _after_ any X-UA-COMPATIBLE HTTP-EQUIV meta tags.
* Otherwise you may hurt IE!
*
* By default this method will return a script wrapped by `<script>` tags, but with
* option `hasToRemoveScriptWrapper` it can send back only the script content
* without the `<script>` wrapper. Useful for React component based frontend.
*
* This method must be called every time you want to generate the headers.
*
* Do *not* reuse the headers between users, or even between requests.
*
* @param {object} options configuration options
* @param {string} [options.nonce] - Nonce to inject into `<script>` header.
* @param {boolean} [options.hasToRemoveScriptWrapper] - Used to import agent script without `<script>` tag wrapper.
* @param {options} [options.allowTransactionlessInjection] Whether or not to allow the Browser Agent to be injected when there is no active transaction
* @returns {string} The script content to be injected in `<head>` or put inside `<script>` tag (depending on options)
*/
API.prototype.getBrowserTimingHeader = function getBrowserTimingHeader(options = {}) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/getBrowserTimingHeader'
)
metric.incrementCallCount()
const trans = this.agent.getTransaction()
const { isValidConfig, failureIdx, quietMode } = validateBrowserMonitoring(
this.agent.config,
trans,
options.allowTransactionlessInjection
)
if (!isValidConfig) {
return _gracefail(failureIdx, quietMode)
}
const config = this.agent.config
// This hash gets written directly into the browser.
const rumHash = {
agent: config.browser_monitoring.js_agent_file,
beacon: config.browser_monitoring.beacon,
errorBeacon: config.browser_monitoring.error_beacon,
licenseKey: config.browser_monitoring.browser_key,
applicationID: config.application_id,
// we don't use these parameters yet
agentToken: null
}
const hasActiveTransaction = trans !== null
if (hasActiveTransaction) {
// bail gracefully outside an ignored transaction
if (trans.isIgnored()) {
return _gracefail(1)
}
/* If we're in an unnamed transaction, add a friendly warning this is to
* avoid people going crazy, trying to figure out why browser monitoring is
* not working when they're missing a transaction name.
*/
const name = trans.getFullName()
if (!name) {
return _gracefail(3)
}
const time = trans.timer.getDurationInMillis()
rumHash.applicationTime = time
/*
* Only the first 13 chars of the license should be used for hashing with
* the transaction name.
*/
const key = config.license_key.substring(0, 13)
rumHash.transactionName = hashes.obfuscateNameUsingKey(name, key)
rumHash.queueTime = trans.queueTime
rumHash.ttGuid = trans.id
const attrs = Object.create(null)
const customAttrs = trans.trace.custom.get(ATTR_DEST.BROWSER_EVENT)
if (!properties.isEmpty(customAttrs)) {
attrs.u = customAttrs
}
const agentAttrs = trans.trace.attributes.get(ATTR_DEST.BROWSER_EVENT)
if (!properties.isEmpty(agentAttrs)) {
attrs.a = agentAttrs
}
if (!properties.isEmpty(attrs)) {
rumHash.atts = hashes.obfuscateNameUsingKey(JSON.stringify(attrs), key)
}
} else {
logger.debug(
'No transaction detected when generating RUM header, continuing without transaction info'
)
}
// if debugging, do pretty format of JSON
const tabs = config.browser_monitoring.debug ? 2 : 0
const json = JSON.stringify(rumHash, null, tabs)
// the complete header to be written to the browser
const out = _generateRUMHeader(
{ nonce: options.nonce, hasToRemoveScriptWrapper: options.hasToRemoveScriptWrapper },
json,
config.browser_monitoring.js_agent_loader
)
logger.trace('generating RUM header', out)
return out
}
/**
* @callback startSegmentCallback
* @param {Function} cb
* The function to time with the created segment.
* @returns {Promise=} Returns a promise if cb returns a promise.
*/
/**
* Wraps the given handler in a segment which may optionally be turned into a
* metric.
*
* @example
* newrelic.startSegment('mySegment', false, function handler() {
* // The returned promise here will signify the end of the segment.
* return myAsyncTask().then(myNextTask)
* })
* @param {string} name
* The name to give the new segment. This will also be the name of the metric.
* @param {boolean} record
* Indicates if the segment should be recorded as a metric. Metrics will show
* up on the transaction breakdown table and server breakdown graph. Segments
* just show up in transaction traces.
* @param {startSegmentCallback} handler
* The function to track as a segment.
* @param {Function} [callback]
* An optional callback for the handler. This will indicate the end of the
* timing if provided.
* @returns {*} Returns the result of calling `handler`.
*/
API.prototype.startSegment = function startSegment(name, record, handler, callback) {
this.agent.metrics
.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/startSegment')
.incrementCallCount()
// Check that we have usable arguments.
if (!name || typeof handler !== 'function') {
logger.warn('Name and handler function are both required for startSegment')
if (typeof handler === 'function') {
return handler(callback)
}
return
}
if (callback && typeof callback !== 'function') {
logger.warn('If using callback, it must be a function')
return handler(callback)
}
// Are we inside a transaction?
if (!this.shim.getActiveSegment()) {
logger.debug('startSegment(%j) called outside of a transaction, not recording.', name)
return handler(callback)
}
assignCLMSymbol(this.shim, handler)
// Create the segment and call the handler.
const wrappedHandler = this.shim.record(handler, function handlerNamer(shim) {
return {
name: name,
recorder: record ? customRecorder : null,
callback: callback ? shim.FIRST : null,
promise: !callback
}
})
return wrappedHandler(callback)
}
/**
* Creates and starts a web transaction to record work done in
* the handle supplied. This transaction will run until the handle
* synchronously returns UNLESS:
* 1. The handle function returns a promise, where the end of the
* transaction will be tied to the end of the promise returned.
* 2. {@link API#getTransaction} is called in the handle, flagging the
* transaction as externally handled. In this case the transaction
* will be ended when {@link TransactionHandle#end} is called in the user's code.
*
* @example
* var newrelic = require('newrelic')
* newrelic.startWebTransaction('/some/url/path', function() {
* var transaction = newrelic.getTransaction()
* setTimeout(function() {
* // do some work
* transaction.end()
* }, 100)
* })
* @param {string} url
* The URL of the transaction. It is used to name and group related transactions in APM,
* so it should be a generic name and not include any variable parameters.
* @param {Function} handle
* Function that represents the transaction work.
* @returns {null|*} Returns null if handle is not a function, otherwise the return value of handle
*/
API.prototype.startWebTransaction = function startWebTransaction(url, handle) {
const metric = this.agent.metrics.getOrCreateMetric(
NAMES.SUPPORTABILITY.API + '/startWebTransaction'
)
metric.incrementCallCount()
if (typeof handle !== 'function') {
logger.warn('startWebTransaction called with a handle arg that is not a function')
return null
}
if (!url) {
logger.warn('startWebTransaction called without a url, transaction not started')
return handle()
}
logger.debug('starting web transaction %s (%s).', url, handle && handle.name)
const shim = this.shim
const tracer = this.agent.tracer
const parent = tracer.getTransaction()
assignCLMSymbol(shim, handle)
return tracer.transactionNestProxy('web', function startWebSegment() {
const tx = tracer.getTransaction()
if (!tx) {
return handle.apply(this, arguments)
}
if (tx === parent) {
logger.debug('not creating nested transaction %s using transaction %s', url, tx.id)
return tracer.addSegment(url, null, null, true, handle)
}
logger.debug(
'creating web transaction %s (%s) with transaction id: %s',
url,
handle && handle.name,
tx.id
)
tx.nameState.setName(NAMES.CUSTOM, null, NAMES.ACTION_DELIMITER, url)
tx.url = url
tx.applyUserNamingRules(tx.url)
tx.baseSegment = tracer.createSegment(url, recordWeb)
tx.baseSegment.start()
const boundHandle = tracer.bindFunction(handle, tx.baseSegment)
maybeAddCLMAttributes(handle, tx.baseSegment)
let returnResult = boundHandle.call(this)
if (returnResult && shim.isPromise(returnResult)) {
returnResult = shim.interceptPromise(returnResult, tx.end.bind(tx))
} else if (!tx.handledExternally) {
logger.debug('Ending unhandled web transaction immediately.')
tx.end()