packages/rum-core/src/common/config-service.js (196 lines of code) (raw):
/**
* MIT License
*
* Copyright (c) 2017-present, Elasticsearch BV
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
import { getCurrentScript, setLabel, merge, extend, isUndefined } from './utils'
import EventHandler from './event-handler'
import { CONFIG_CHANGE, LOCAL_CONFIG_KEY } from './constants'
function getConfigFromScript() {
var script = getCurrentScript()
var config = getDataAttributesFromNode(script)
return config
}
function getDataAttributesFromNode(node) {
if (!node) {
return {}
}
var dataAttrs = {}
var dataRegex = /^data-([\w-]+)$/
var attrs = node.attributes
for (var i = 0; i < attrs.length; i++) {
var attr = attrs[i]
if (dataRegex.test(attr.nodeName)) {
var key = attr.nodeName.match(dataRegex)[1]
// camelCase key
var camelCasedkey = key
.split('-')
.map((value, index) => {
return index > 0
? value.charAt(0).toUpperCase() + value.substring(1)
: value
})
.join('')
dataAttrs[camelCasedkey] = attr.value || attr.nodeValue
}
}
return dataAttrs
}
class Config {
constructor() {
this.config = {
serviceName: '',
serviceVersion: '',
environment: '',
serverUrl: 'http://localhost:8200',
serverUrlPrefix: '',
active: true,
instrument: true,
disableInstrumentations: [],
logLevel: 'warn',
breakdownMetrics: false,
ignoreTransactions: [],
eventsLimit: 80,
queueLimit: -1,
flushInterval: 500,
distributedTracing: true,
distributedTracingOrigins: [],
distributedTracingHeaderName: 'traceparent',
pageLoadTraceId: '',
pageLoadSpanId: '',
pageLoadSampled: false,
pageLoadParentId: '',
pageLoadTransactionName: '',
propagateTracestate: false,
transactionSampleRate: 1.0,
centralConfig: false,
monitorLongtasks: true,
apiVersion: 2,
context: {},
session: false,
apmRequest: null,
sendCredentials: false
}
this.events = new EventHandler()
this.filters = []
/**
* Packages that uses rum-core under the hood must override
* the version via setVersion
*/
this.version = ''
}
init() {
var scriptData = getConfigFromScript()
this.setConfig(scriptData)
}
setVersion(version) {
this.version = version
}
addFilter(cb) {
if (typeof cb !== 'function') {
throw new Error('Argument to must be function')
}
this.filters.push(cb)
}
applyFilters(data) {
for (var i = 0; i < this.filters.length; i++) {
data = this.filters[i](data)
if (!data) {
return
}
}
return data
}
get(key) {
return key.split('.').reduce((obj, objKey) => {
return obj && obj[objKey]
}, this.config)
}
setUserContext(userContext = {}) {
const context = {}
const { id, username, email } = userContext
if (typeof id === 'number' || typeof id === 'string') {
context.id = id
}
if (typeof username === 'string') {
context.username = username
}
if (typeof email === 'string') {
context.email = email
}
this.config.context.user = extend(this.config.context.user || {}, context)
}
setCustomContext(customContext = {}) {
this.config.context.custom = extend(
this.config.context.custom || {},
customContext
)
}
addLabels(tags) {
if (!this.config.context.tags) {
this.config.context.tags = {}
}
var keys = Object.keys(tags)
keys.forEach(k => setLabel(k, tags[k], this.config.context.tags))
}
setConfig(properties = {}) {
let { transactionSampleRate, serverUrl } = properties
/**
* Normalize config
*
* Remove all trailing slash for serverUrl since SERVER_URL_PREFIX
* already includes a forward slash for the path
*/
if (serverUrl) {
properties.serverUrl = serverUrl.replace(/\/+$/, '')
}
if (!isUndefined(transactionSampleRate)) {
if (transactionSampleRate < 0.0001 && transactionSampleRate > 0) {
transactionSampleRate = 0.0001
}
properties.transactionSampleRate =
Math.round(transactionSampleRate * 10000) / 10000
}
this.config = merge(this.config, properties)
this.events.send(CONFIG_CHANGE, [this.config])
}
/**
* Validate the config aganist the required parameters and
* generates error messages with missing and invalid keys
*/
validate(properties = {}) {
const requiredKeys = ['serviceName', 'serverUrl']
const allKeys = Object.keys(this.config)
const errors = {
missing: [],
invalid: [],
unknown: []
}
/**
* Check when required keys are missing or unknown keys found
*/
Object.keys(properties).forEach(key => {
if (requiredKeys.indexOf(key) !== -1 && !properties[key]) {
errors.missing.push(key)
}
if (allKeys.indexOf(key) === -1) {
errors.unknown.push(key)
}
})
/**
* Invalid values on the config
*/
if (
properties.serviceName &&
!/^[a-zA-Z0-9 _-]+$/.test(properties.serviceName)
) {
errors.invalid.push({
key: 'serviceName',
value: properties.serviceName,
allowed: 'a-z, A-Z, 0-9, _, -, <space>'
})
}
const sampleRate = properties.transactionSampleRate
if (
typeof sampleRate !== 'undefined' &&
(typeof sampleRate !== 'number' ||
isNaN(sampleRate) ||
sampleRate < 0 ||
sampleRate > 1)
) {
errors.invalid.push({
key: 'transactionSampleRate',
value: sampleRate,
allowed: 'Number between 0 and 1'
})
}
return errors
}
/**
* The localStorage is needed for session feature
* but since this is a breaking change for central config,
* we're using the session flag until the next major version
* which should remove the use of sessionStorage.
*/
getLocalConfig() {
let storage = sessionStorage
if (this.config.session) {
storage = localStorage
}
let config = storage.getItem(LOCAL_CONFIG_KEY)
if (config) {
return JSON.parse(config)
}
}
setLocalConfig(config, merge) {
if (config) {
if (merge) {
const prevConfig = this.getLocalConfig()
config = { ...prevConfig, ...config }
}
let storage = sessionStorage
if (this.config.session) {
storage = localStorage
}
storage.setItem(LOCAL_CONFIG_KEY, JSON.stringify(config))
}
}
dispatchEvent(name, args) {
this.events.send(name, args)
}
observeEvent(name, fn) {
return this.events.observe(name, fn)
}
}
export default Config