├── packages ├── react │ ├── src │ │ ├── react-app-env.d.ts │ │ ├── .eslintrc │ │ ├── index.js │ │ ├── index.test.js │ │ ├── web-tracer.js │ │ └── instrumentation │ │ │ └── redirectInstrumentation.js │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── docs │ │ ├── instrumentation.md │ │ └── usage.md │ ├── package.json │ └── README.md └── web │ ├── .gitignore │ ├── src │ ├── .eslintrc │ ├── index.d.ts │ ├── index.js │ ├── ip-calculator.js │ ├── consts.js │ ├── instrumentation │ │ ├── redirectInstrumentation.js │ │ ├── xmlHttpInstrumentation.js │ │ ├── documentLoadInstrumentation.js │ │ └── fetchInstrumentation.js │ ├── formatter.js │ ├── resource-manager.js │ ├── utils.js │ ├── web-tracer.js │ └── exporter.js │ ├── .eslintrc.js │ ├── .npmignore │ ├── webpack.config.js │ ├── docs │ ├── usage.md │ ├── instrumentations.md │ ├── exporter.md │ └── web-tracer.md │ ├── tsconfig.json │ ├── test │ ├── disabled-error-collection.test.js │ ├── helper.js │ ├── redirect.test.js │ ├── requests.test.js │ ├── web-tracer.test.js │ └── docLoad.test.js │ ├── package.json │ └── README.md ├── commitlint.config.js ├── .gitignore ├── .husky └── commit-msg ├── .github └── workflows │ └── ci.yml ├── package.json ├── LICENSE ├── CODE_OF_CONDUCT.md └── README.md /packages/react/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/web/.gitignore: -------------------------------------------------------------------------------- 1 | npm/node_modules/ 2 | sample/node_modules/ 3 | node_modules -------------------------------------------------------------------------------- /packages/web/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-angular']} 2 | 3 | -------------------------------------------------------------------------------- /packages/react/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb-base', 3 | }; 4 | -------------------------------------------------------------------------------- /packages/react/src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/web/src/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as Epsagon from "./web-tracer"; 2 | export default Epsagon; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | **node_modules/ 3 | **dist/ 4 | **/.DS_Store 5 | .vscode 6 | .env 7 | *.cpuprofile 8 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /packages/react/src/index.js: -------------------------------------------------------------------------------- 1 | import { init, identify, tag } from './web-tracer'; 2 | 3 | const Epsagon = { init, identify, tag }; 4 | 5 | export default Epsagon; 6 | -------------------------------------------------------------------------------- /packages/web/src/index.js: -------------------------------------------------------------------------------- 1 | import { init, identify, tag } from './web-tracer'; 2 | 3 | const Epsagon = { init, identify, tag }; 4 | 5 | export default Epsagon; 6 | -------------------------------------------------------------------------------- /packages/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb-base', 3 | ignorePatterns: ['*.test.js', '**/dist/*.js', '**/test/*.js'], 4 | rules: { 'max-len': ['error', { code: 120 }] }, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react/src/index.test.js: -------------------------------------------------------------------------------- 1 | import Epsagon from '.'; 2 | 3 | describe('Sanity test', () => { 4 | it('init function exists', () => { 5 | expect(typeof Epsagon.init === 'function').toEqual(true); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/web/.npmignore: -------------------------------------------------------------------------------- 1 | ## the src folder 2 | .babelrc 3 | rollup.config.js 4 | ## node modules folder 5 | node_modules 6 | ## incase you have a git repositiory initiated 7 | .git 8 | .gitignore 9 | CVS 10 | .svn 11 | .hg 12 | .lock-wscript 13 | .wafpickle-N 14 | .DS_Store 15 | npm-debug.log 16 | .npmrc 17 | 18 | config.gypi 19 | package-lock.json -------------------------------------------------------------------------------- /packages/web/webpack.config.js: -------------------------------------------------------------------------------- 1 | // webpack.config.js 2 | module.exports = { 3 | entry: [ './src/index.js' ], 4 | output: { 5 | filename: 'bundle.js', 6 | libraryTarget: 'var', 7 | library: 'Epsagon' 8 | }, 9 | mode: "production", 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.js$/, 14 | exclude: /(node_modules|\.test\.[tj]sx?$)/, 15 | } 16 | ] 17 | }, 18 | resolve: { extensions: ['.ts', '.js', '.json'] }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/react/src/web-tracer.js: -------------------------------------------------------------------------------- 1 | import ReactRedirectInstrumentation from './instrumentation/redirectInstrumentation'; 2 | 3 | const epsagon = require('@epsagon/web'); 4 | 5 | const epsagonInit = epsagon.init; 6 | const { identify, tag } = epsagon; 7 | 8 | function init(configData) { 9 | const { tracer, epsSpan } = epsagonInit(configData); 10 | 11 | if (configData.history) { 12 | ReactRedirectInstrumentation(configData.history, tracer, epsSpan); 13 | } 14 | 15 | return tracer; 16 | } 17 | 18 | export { init, identify, tag }; 19 | -------------------------------------------------------------------------------- /packages/web/docs/usage.md: -------------------------------------------------------------------------------- 1 | # Epsagon Web Tracing 2 | 3 | The epsagon web tracing module is created to provide front end automated instrumentation to web apps built using nodejs. This package provides tracing and instrumentation for document load events, and http requests sent through xmlhttp or fetch. It then customizes the traces for epsagon specifications before sending to the Epsagon backend. 4 | 5 | ## Set Up 6 | 7 | ```bash 8 | npm install --save ep-react-logs 9 | ``` 10 | 11 | ```js 12 | const Epsagon = require('ep-react-logs') 13 | 14 | Epsagon.init({ 15 | app_name: 'app name', 16 | token: 'token string', 17 | }) 18 | ``` 19 | 20 | -------------------------------------------------------------------------------- /packages/web/docs/instrumentations.md: -------------------------------------------------------------------------------- 1 | # Epsagon Instrumentations 2 | 3 | The base web instrumentations are based on opentelemetry's web instrumentations. In order to send additional data to what opentelemetry send by default, some instrumentations are extended in the instrumentation folder. Currently we are grabbing extra data from both the request and the response and adding them as additional span attributes. 4 | 5 | To add more span attributes before it gets sent to the exporter, use the ```span.setAttribute()``` function. 6 | 7 | ```js 8 | span.setAttribute('http.response.body', (await resCopy.text()).substring(0, 5000)); 9 | ``` 10 | 11 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/*" 4 | ], 5 | "compilerOptions": { 6 | "allowJs": true, 7 | "declaration": true, 8 | "outDir": "dist", 9 | "target": "es5", 10 | "lib": [ 11 | "dom", 12 | "dom.iterable", 13 | "esnext" 14 | ], 15 | "skipLibCheck": true, 16 | "esModuleInterop": true, 17 | "allowSyntheticDefaultImports": true, 18 | "strict": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "module": "esnext", 22 | "moduleResolution": "node", 23 | "resolveJsonModule": true, 24 | "isolatedModules": true, 25 | "jsx": "react" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "allowJs": true, 7 | "declaration": true, 8 | "emitDeclarationOnly": false, 9 | "outDir": "dist", 10 | "target": "es5", 11 | "lib": [ 12 | "dom", 13 | "dom.iterable", 14 | "esnext" 15 | ], 16 | "skipLibCheck": true, 17 | "esModuleInterop": true, 18 | "allowSyntheticDefaultImports": true, 19 | "strict": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "module": "esnext", 23 | "moduleResolution": "node", 24 | "resolveJsonModule": true, 25 | "isolatedModules": true, 26 | "noEmit": true, 27 | "jsx": "react-jsx" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/react/docs/instrumentation.md: -------------------------------------------------------------------------------- 1 | # Epsagon Instrumentation 2 | 3 | The base web instrumentations are configured in the Epsagon js web package. In addition Epsagon's custom redirect instrumentation is configured in the instrumentation folder. 4 | 5 | React's synthetic events take the place of any native web events when react pages update or change routes. In order to catch the route changes, the history component must be passed into the init function and then the redirect instrumentation listens for changes which are turned into new spans with the operation "route_change". 6 | 7 | To add more span attributes to the route change events before they get sent to the exporter, use the ```span.setAttribute()``` function. 8 | 9 | ```js 10 | span.setAttribute("action", action) 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | ci: 9 | name: CI 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 12 20 | 21 | - name: Install dependencies @epsagon/web and run tests 22 | working-directory: ./packages/web 23 | run: npm install && npm run test && npm run build 24 | 25 | - name: Release @epsagon/web 26 | if: ${{ github.event_name == 'push' }} 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 30 | working-directory: ./packages/web 31 | run: npm run semantic-release 32 | -------------------------------------------------------------------------------- /packages/web/docs/exporter.md: -------------------------------------------------------------------------------- 1 | # Epsagon Exporter 2 | 3 | The Epsagon exporter extends opentelemtry's exporter but customizes the data to Epsagon specifications before it send the data out. This includes updating the event names, types, operations and adding non-event specific data such as user agent data and current url data. 4 | 5 | To add more data to the spans before they are sent out, it must be added to the end of either the current resource attributes or the span attributes, with the attribute name, type and value set. 6 | 7 | ```js 8 | attributesLength ++ 9 | spanAttributes[attributesLength] = { key: 'attribute name', value: {stringValue: 'attribute value'}} 10 | 11 | resourcesLength ++ 12 | convertedSpans.resourceSpans[0].resource.attributes[resourcesLength] = { key: 'attribute name', value: {stringValue: 'attribute value'}} 13 | ``` 14 | 15 | Data such as where the exporter sends the spans can be customized in the init function of the web-tracer file. 16 | -------------------------------------------------------------------------------- /packages/web/src/ip-calculator.js: -------------------------------------------------------------------------------- 1 | const API_KEY = 'PfW1i6IVetxP8Xu'; 2 | 3 | class EpsagonIPCalculator { 4 | static calculate(callback) { 5 | // start requesting for ip 6 | /* eslint-disable no-undef */ 7 | fetch('https://api.ipify.org?format=json', { 8 | eps: true, // added to negate span creation 9 | }) 10 | .then((response) => response.json()) 11 | .then((data) => { 12 | /* eslint-disable no-undef */ 13 | fetch(`https://pro.ip-api.com/json/${data.ip}?fields=16409&key=${API_KEY}`, { 14 | eps: true, // added to negate span creation 15 | }) 16 | .then((response2) => response2.json()) 17 | .then((data2) => { 18 | callback({ 19 | ip: data.ip, 20 | country: data2.country, 21 | regionName: data2.regionName, 22 | city: data2.city, 23 | }); 24 | }); 25 | }); 26 | } 27 | } 28 | 29 | export default EpsagonIPCalculator; 30 | -------------------------------------------------------------------------------- /packages/web/docs/web-tracer.md: -------------------------------------------------------------------------------- 1 | # Epsagon Web Tracing 2 | 3 | The library is built using the opentelemetry javascript libraries. The opentelemetry functionalities as well as any custom epsagon functionalities are set up in the init function of the web-tracer file. 4 | Any additional instrumentations (such as user interaction) can be added there. 5 | 6 | ```js 7 | registerInstrumentations({ 8 | tracerProvider: provider, 9 | instrumentations: [ 10 | new DocumentLoadInstrumentation(), 11 | new EpsagonFetchInstrumentation(), 12 | // new UserInteractionInstrumentation(), 13 | new EpsagonXMLHttpRequestInstrumentation() 14 | ], 15 | }); 16 | ``` 17 | 18 | Additional data to be sent through as a header to the Epsagon backend can also be set there. 19 | 20 | ```js 21 | const collectorOptions = { 22 | serviceName: configData.app_name, 23 | url: configData.url, 24 | headers: { 25 | "X-Epsagon-Token": `${configData.token}`, 26 | }, 27 | }; 28 | ``` -------------------------------------------------------------------------------- /packages/react/docs/usage.md: -------------------------------------------------------------------------------- 1 | # Epsagon React Tracing 2 | 3 | The Epsagon react tracing module is created to provide front end automated instrumentation to react apps. This package extends the Epsagon web tracing module that provides tracing and instrumentation for document load events, and http requests sent through xmlhttp or fetch and introduces additional redirect instrumentation. 4 | 5 | ## Set Up 6 | 7 | ```bash 8 | npm install --save ep-react-logs 9 | ``` 10 | 11 | ```jsx 12 | import React, { Component } from 'react' 13 | import { createBrowserHistory } from 'history'; 14 | const Epsagon = require('ep-react-logs') 15 | const history = createBrowserHistory(); 16 | 17 | Epsagon.init({ 18 | app_name: 'app name', 19 | token: 'epsagon token', 20 | history: history 21 | }) 22 | 23 | ReactDOM.render( 24 | 25 |
26 | 27 | 28 |
29 |
30 | 31 | ,document.getElementById('root')); 32 | ``` 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "epsagon-browser", 3 | "version": "1.0.0", 4 | "description": "


", 5 | "main": "commitlint.config.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "husky": { 10 | "hooks": { 11 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 12 | } 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/epsagon/epsagon-browser.git" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/epsagon/epsagon-browser/issues" 22 | }, 23 | "homepage": "https://github.com/epsagon/epsagon-browser#readme", 24 | "devDependencies": { 25 | "husky": "^7.0.2", 26 | "@commitlint/cli": "^9.1.2", 27 | "@commitlint/config-angular": "^7.1.2", 28 | "@commitlint/config-conventional": "^7.1.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Epsagon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/web/test/disabled-error-collection.test.js: -------------------------------------------------------------------------------- 1 | import EpsagonDocumentLoadInstrumentation from '../src/instrumentation/documentLoadInstrumentation'; 2 | 3 | const chai = require('chai'); 4 | const sinon = require('sinon'); 5 | const helper = require('./helper'); 6 | const epsagon = require('../src/web-tracer'); 7 | 8 | const sandbox = sinon.createSandbox(); 9 | let spyCreateSpan; 10 | 11 | describe('Error Collection', () => { 12 | beforeEach(() => { 13 | helper.browserenv({errorDisabled: true}); 14 | spyCreateSpan = sandbox.spy(EpsagonDocumentLoadInstrumentation.prototype, 'reportError'); 15 | 16 | Object.defineProperty(global.window.document, 'readyState', { 17 | writable: true, 18 | value: 'complete', 19 | }); 20 | }); 21 | 22 | afterEach(() => { 23 | spyCreateSpan.restore(); 24 | }); 25 | 26 | it('should not create a span for a raised error', (done) => { 27 | epsagon.init({isTest: true, token: 'abcdef', errorDisabled: true}) 28 | helper.createError(); 29 | setTimeout(() => { 30 | chai.expect(spyCreateSpan.callCount).to.equal(0); 31 | done(); 32 | }, 0); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/web/src/consts.js: -------------------------------------------------------------------------------- 1 | const VERSION = require('../package.json').version; 2 | 3 | const DEFAULT_CONFIGURATIONS = { 4 | appName: 'Epsagon Application', 5 | collectorURL: 'https://opentelemetry.tc.epsagon.com/traces', 6 | pageLoadTimeout: 30000, 7 | redirectTimeout: 3000, 8 | maxBatchSize: 1024, 9 | maxQueueSize: 2048, 10 | scheduledDelayMillis: 5000, 11 | exportTimeoutMillis: 30000, 12 | networkSamplingRatio: 1, 13 | }; 14 | 15 | const ROOT_TYPE = { 16 | EPS: 'epsagon_init', 17 | DOC: 'document_load', 18 | REDIR: 'redirect', 19 | ROOT_TYPE_DOC: 'doc', 20 | ERROR: 'error', 21 | EXCEPTION: 'exception', 22 | }; 23 | 24 | const SPAN_ATTRIBUTES_NAMES = { 25 | BROWSER_HOST: 'browser.host', 26 | BROWSER_PATH: 'browser.path', 27 | HOST_HEADER: 'http.host', 28 | HOST_USER_AGENT: 'http.user_agent', 29 | HOST_REQUEST_USER_AGENT: 'http.request.headers.User-Agent', 30 | DOCUMENT_LOAD: 'document-load', 31 | DOCUMENT_LOAD_SPAN_NAME: 'documentLoad', 32 | REACT_COMPONENT_NAME: 'react_component_name', 33 | USER_INTERACTION: 'user-interaction', 34 | ROUTE_CHANGE: 'route_change', 35 | RESPONSE_CONTENT_LENGTH: 'http.response_content_length', 36 | RESPONSE_CONTENT_LENGTH_EPS: 'http.response_content_length_eps', 37 | EXCEPTION_MESSAGE: 'exception.message', 38 | EXCEPTION_TYPE: 'exception.type', 39 | EXCEPTION_STACK: 'exception.stacktrace', 40 | MESSAGE: 'message', 41 | }; 42 | 43 | export { 44 | VERSION, DEFAULT_CONFIGURATIONS, ROOT_TYPE, SPAN_ATTRIBUTES_NAMES, 45 | }; 46 | -------------------------------------------------------------------------------- /packages/web/src/instrumentation/redirectInstrumentation.js: -------------------------------------------------------------------------------- 1 | import EpsagonUtils from '../utils'; 2 | import { diag } from "@opentelemetry/api"; 3 | 4 | /* eslint-disable no-undef */ 5 | class EpsagonRedirectInstrumentation { 6 | constructor(tracer, parentSpan, resetTimer) { 7 | this.parentSpan = parentSpan; 8 | this.parentSpan.path = `${window.location.pathname}${window.location.hash}`; 9 | this.tracer = tracer; 10 | setInterval(() => { 11 | const currentPath = `${window.location.pathname}${window.location.hash}`; 12 | if (this.parentSpan.path && currentPath !== parentSpan.path) { 13 | this.createRouteChangeSpan(this.parentSpan.path, currentPath); 14 | this.parentSpan.path = currentPath; 15 | } 16 | }, resetTimer); 17 | } 18 | 19 | createRouteChangeSpan(oldPath, newPath) { 20 | const span = this.tracer.startSpan('route_change', { 21 | attributes: { 22 | operation: 'route_change', 23 | type: 'browser', 24 | previousPage: oldPath, 25 | path: newPath, 26 | 'http.url': window.location.href, 27 | }, 28 | }); 29 | this.parentSpan.currentSpan = span; 30 | if (window.location.hash) { 31 | span.setAttribute('hash', window.location.hash); 32 | } 33 | EpsagonUtils.addEpsSpanAttrs(span, this.parentSpan); 34 | span.setStatus({ code: 0 }); 35 | diag.debug('create span for redirect', span); 36 | span.end(); 37 | } 38 | } 39 | 40 | export default EpsagonRedirectInstrumentation; 41 | -------------------------------------------------------------------------------- /packages/react/src/instrumentation/redirectInstrumentation.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len: ["error", { "ignoreComments": true }] */ 2 | 3 | /* eslint no-param-reassign: ["error", { "props": true, "ignorePropertyModificationsFor": ["parentSpan"] }] */ 4 | const { EpsagonUtils } = require('@epsagon/web'); 5 | 6 | function ReactRedirectInstrumentation(history, tracer, parentSpan) { 7 | const startSpan = (originPath, newPath) => tracer.startSpan('route_change', { 8 | attributes: { 9 | operation: 'route_change', 10 | type: 'browser', 11 | previousPage: originPath, 12 | path: newPath, 13 | }, 14 | }); 15 | 16 | history.listen((location, action) => { 17 | /* eslint-disable no-undef */ 18 | const currentPath = `${location.pathname}${window.location.hash}`; 19 | if (action 20 | && (action.toLowerCase() === 'push' || action.toLowerCase() === 'pop') 21 | && (parentSpan.path !== currentPath)) { 22 | const span = startSpan(parentSpan.path, location.pathname); 23 | parentSpan.currentSpan = span; 24 | if (location.hash && location.hash.length) { 25 | span.setAttribute('hash', location.hash); 26 | } 27 | if (location.search && location.search.length) { 28 | span.setAttribute('search', location.search); 29 | } 30 | if (location.state && location.state.length) { 31 | span.setAttribute('history.state', location.state); 32 | } 33 | span.setAttribute('action', action); 34 | span.setStatus({ code: 0 }); 35 | EpsagonUtils.addEpsSpanAttrs(span, parentSpan); 36 | span.end(); 37 | parentSpan.path = currentPath; 38 | } 39 | }); 40 | } 41 | 42 | export default ReactRedirectInstrumentation; 43 | -------------------------------------------------------------------------------- /packages/web/src/formatter.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import EpsagonUtils from './utils'; 3 | 4 | class EpsagonFormatter { 5 | constructor(config) { 6 | this.config = config; 7 | } 8 | 9 | static formatRouteChangeSpan(userAgent) { 10 | /* eslint-disable no-undef */ 11 | return { 12 | name: `${window.location.pathname}${window.location.hash}`, 13 | obj: { key: 'http.request.headers.User-Agent', value: { stringValue: JSON.stringify(userAgent).replace(/"([^"]+)":/g, '$1:') } }, 14 | }; 15 | } 16 | 17 | static formatDocumentLoadSpan() { 18 | return { 19 | name: `${window.location.pathname}${window.location.hash}`, 20 | browser: { key: 'type', value: { stringValue: 'browser' } }, 21 | operation: { key: 'operation', value: { stringValue: 'page_load' } }, 22 | }; 23 | } 24 | 25 | static formatUserInteractionSpan(spanAttributes) { 26 | const eventType = spanAttributes.filter((attr) => attr.key === ('event_type')); 27 | return { 28 | operation: { key: 'operation', value: { stringValue: eventType[0].value.stringValue } }, 29 | userInteraction: { key: 'type', value: { stringValue: 'user-interaction' } }, 30 | }; 31 | } 32 | 33 | formatHttpRequestSpan(_span, httpHost, _spanAttributes, _attributesLength) { 34 | const span = _span; 35 | let spanAttributes = _spanAttributes; 36 | let attributesLength = _attributesLength; 37 | span.name = httpHost[0].value.stringValue; 38 | spanAttributes[attributesLength] = { key: 'type', value: { stringValue: 'http' } }; 39 | attributesLength += 1; 40 | 41 | if (!this.config.metadataOnly) { 42 | const httpContentLength = spanAttributes.filter((attr) => attr.key === 'http.response_content_length'); 43 | const epsHttpContentLength = spanAttributes.filter((attr) => attr.key === 'http.response_content_length_eps'); 44 | if (epsHttpContentLength.length > 0) { 45 | httpContentLength[0].value.intValue = epsHttpContentLength[0].value.intValue; 46 | } 47 | } 48 | 49 | const httpUrlAttr = spanAttributes.filter((attr) => attr.key === 'http.url'); 50 | const httpUrl = httpUrlAttr[0].value.stringValue; 51 | 52 | const afterParse = EpsagonUtils.parseURL( 53 | httpUrl, span, spanAttributes, attributesLength, this.config.metadataOnly, 54 | ); 55 | attributesLength = afterParse.attributesLength; 56 | spanAttributes = afterParse.spanAttributes; 57 | return { span, attributesLength, spanAttributes }; 58 | } 59 | } 60 | 61 | export default EpsagonFormatter; 62 | -------------------------------------------------------------------------------- /packages/web/test/helper.js: -------------------------------------------------------------------------------- 1 | const { JSDOM } = require('jsdom'); 2 | const fetch = require('node-fetch'); 3 | const sinon = require('sinon'); 4 | import { diag } from "@opentelemetry/api"; 5 | const epsagon = require('../src/web-tracer'); 6 | 7 | 8 | class Request { 9 | constructor() { 10 | diag.debug('doesnt matter'); 11 | } 12 | } 13 | 14 | const defaults = { 15 | errorDisabled: false 16 | }; 17 | 18 | /** 19 | * Simulate browser environment for nodejs. 20 | */ 21 | module.exports.browserenv = function (options = defaults) { 22 | const cfg = { url: 'http://localhost' }; 23 | const dom = new JSDOM('', cfg); 24 | global.window = dom.window; 25 | global.document = dom.window.document; 26 | 27 | Object.keys(global.window).forEach((property) => { 28 | if (typeof global[property] === 'undefined') { 29 | global[property] = global.window[property]; 30 | } 31 | }); 32 | 33 | global.Element = window.Element; 34 | // global.Image = window.Image; 35 | // // maybe more of: global.Whatever = window.Whatever 36 | 37 | global.navigator = { 38 | userAgent: 'node.js', 39 | }; 40 | 41 | globalThis.fetch = fetch; 42 | globalThis.window.fetch = globalThis.fetch; 43 | 44 | global.document.getElementsByTagName = (name = meta) => [{ 45 | name: 'page name', 46 | getAttribute: () => name, 47 | }]; 48 | 49 | globalThis.performance = global.window.performance; 50 | globalThis.window = global.window; 51 | globalThis.document = global.document; 52 | 53 | globalThis.XMLHttpRequest = sinon.useFakeXMLHttpRequest(); 54 | const requests = this.requests = []; 55 | 56 | globalThis.XMLHttpRequest.onCreate = function (xhr) { 57 | requests.push(xhr); 58 | }; 59 | 60 | globalThis.Request = Request; 61 | 62 | let initArgs = { 63 | ...{ token: 'dfsaf', isTest: true }, 64 | ...options 65 | }; 66 | 67 | const res = epsagon.init(initArgs); 68 | return res.epsSpan; 69 | }; 70 | 71 | module.exports.createError = () => { 72 | const e = new window.ErrorEvent('error', { error: { message: 'my error', type: 'my error type' }, message: 'myerror' }); 73 | window.dispatchEvent(e); 74 | }; 75 | 76 | module.exports.createEmptyStackError = () => { 77 | const err = new Error(); 78 | err.message = 'my error'; 79 | err.type = 'my error type'; 80 | err.stack = {}; 81 | const e = new window.ErrorEvent('error', {error :err}); 82 | window.dispatchEvent(e); 83 | }; 84 | 85 | module.exports.restore = () => { 86 | globalThis.XMLHttpRequest.restore(); 87 | }; 88 | 89 | module.exports.type = { 90 | DOC: 'browser', 91 | HTTP: 'http', 92 | }; 93 | 94 | module.exports.operations = { 95 | LOAD: 'page_load', 96 | ROUTE: 'route_change', 97 | }; 98 | -------------------------------------------------------------------------------- /packages/web/src/resource-manager.js: -------------------------------------------------------------------------------- 1 | import { VERSION } from './consts'; 2 | 3 | class EpsagonResourceManager { 4 | constructor(config) { 5 | this.config = config; 6 | } 7 | 8 | addResourceAttrs(_convertedSpans, userAgent) { 9 | const convertedSpans = _convertedSpans; 10 | 11 | const appName = this.config.serviceName; 12 | let resourcesLength = convertedSpans.resourceSpans[0].resource.attributes.length; 13 | 14 | // WE CANT USE THE NATIVE OPENTELEMETRY ADDATTRIBUTE SINCE IT WILL SHUTDOWN THE SPAN 15 | convertedSpans.resourceSpans[0].resource.attributes[resourcesLength] = { key: 'application', value: { stringValue: appName } }; 16 | resourcesLength += 1; 17 | convertedSpans.resourceSpans[0].resource.attributes[resourcesLength] = { key: 'browser.name', value: { stringValue: userAgent.browser.name } }; 18 | resourcesLength += 1; 19 | convertedSpans.resourceSpans[0].resource.attributes[resourcesLength] = { key: 'browser.version', value: { stringValue: userAgent.browser.version } }; 20 | resourcesLength += 1; 21 | convertedSpans.resourceSpans[0].resource.attributes[resourcesLength] = { key: 'browser.operating_system', value: { stringValue: userAgent.os.name } }; 22 | resourcesLength += 1; 23 | convertedSpans.resourceSpans[0].resource.attributes[resourcesLength] = { key: 'browser.operating_system_version', value: { stringValue: userAgent.os.version } }; 24 | resourcesLength += 1; 25 | 26 | // ADD VERSION IF EXISTS 27 | if (VERSION) { 28 | convertedSpans.resourceSpans[0].resource.attributes[resourcesLength] = { key: 'epsagon.version', value: { stringValue: VERSION } }; 29 | resourcesLength += 1; 30 | } 31 | 32 | // ADD IP IF EXISTS 33 | if (userAgent.browser.ip) { 34 | convertedSpans.resourceSpans[0].resource.attributes[resourcesLength] = { key: 'user.ip', value: { stringValue: userAgent.browser.ip } }; 35 | resourcesLength += 1; 36 | } 37 | // ADD COUNTRY IF EXISTS 38 | if (userAgent.browser.country) { 39 | convertedSpans.resourceSpans[0].resource.attributes[resourcesLength] = { key: 'user.country', value: { stringValue: userAgent.browser.country } }; 40 | resourcesLength += 1; 41 | } 42 | 43 | // ADD CITY IF EXISTS 44 | if (userAgent.browser.city) { 45 | convertedSpans.resourceSpans[0].resource.attributes[resourcesLength] = { key: 'user.city', value: { stringValue: userAgent.browser.city } }; 46 | resourcesLength += 1; 47 | } 48 | 49 | // ADD REGION IF EXISTS 50 | if (userAgent.browser.regionName) { 51 | convertedSpans.resourceSpans[0].resource.attributes[resourcesLength] = { key: 'user.region', value: { stringValue: userAgent.browser.regionName } }; 52 | resourcesLength += 1; 53 | } 54 | 55 | // remove undefined service.name attr 56 | convertedSpans.resourceSpans[0].resource.attributes = convertedSpans.resourceSpans[0].resource.attributes.filter((attr) => attr.key !== ['service.name']); 57 | return convertedSpans; 58 | } 59 | } 60 | 61 | export default EpsagonResourceManager; 62 | -------------------------------------------------------------------------------- /packages/web/test/redirect.test.js: -------------------------------------------------------------------------------- 1 | import EpsagonExporter from '../src/exporter'; 2 | 3 | const chai = require('chai'); 4 | const sinon = require('sinon'); 5 | const helper = require('./helper'); 6 | 7 | const epsSpan = helper.browserenv(); 8 | const sandbox = sinon.createSandbox(); 9 | 10 | let spyExporter; 11 | 12 | describe('redirect instrumentation', () => { 13 | beforeEach(() => { 14 | Object.defineProperty(global.window.document, 'readyState', { 15 | writable: true, 16 | value: 'complete', 17 | }); 18 | spyExporter = sandbox.spy(EpsagonExporter.prototype, 'convert'); 19 | }); 20 | 21 | afterEach(() => { 22 | spyExporter.restore(); 23 | }); 24 | 25 | it('should create span for changed path', (done) => { 26 | const oldPath = '/new-path'; 27 | epsSpan.path = oldPath; 28 | setTimeout(() => { 29 | const spans = spyExporter.returnValues[0]; 30 | chai.assert.ok(spans.resourceSpans[0].instrumentationLibrarySpans[0], 'spans not created'); 31 | const span = spans.resourceSpans[0].instrumentationLibrarySpans[0]; 32 | chai.assert.equal(span.spans.length, 1, 'more then one span being created'); 33 | chai.assert.equal(span.spans[0].name, '/', 'Span name was not converted to path name'); 34 | const typeObj = span.spans[0].attributes.filter((obj) => obj.key === 'type'); 35 | const operationObj = span.spans[0].attributes.filter((obj) => obj.key === 'operation'); 36 | const pathObj = span.spans[0].attributes.filter((obj) => obj.key === 'previousPage'); 37 | chai.assert.equal(typeObj[0].value.stringValue, helper.type.DOC, 'incorrect redirect type'); 38 | chai.assert.equal(operationObj[0].value.stringValue, helper.operations.ROUTE, 'incorrect operation'); 39 | chai.assert.equal(pathObj[0].value.stringValue, oldPath, 'not logging old path correctly'); 40 | done(); 41 | }, 7000); 42 | }).timeout(8000); 43 | 44 | it('should create span for changed hash', (done) => { 45 | window.location.hash = '#test'; 46 | setTimeout(() => { 47 | const spans = spyExporter.returnValues[0]; 48 | chai.assert.ok(spans.resourceSpans[0].instrumentationLibrarySpans[0], 'spans not created'); 49 | const span = spans.resourceSpans[0].instrumentationLibrarySpans[0]; 50 | chai.assert.equal(span.spans.length, 1, 'more then one span being created'); 51 | chai.assert.equal(span.spans[0].name, '/#test', 'Span name was not converted to path name'); 52 | const typeObj = span.spans[0].attributes.filter((obj) => obj.key === 'type'); 53 | const operationObj = span.spans[0].attributes.filter((obj) => obj.key === 'operation'); 54 | const pathObj = span.spans[0].attributes.filter((obj) => obj.key === 'previousPage'); 55 | chai.assert.equal(typeObj[0].value.stringValue, helper.type.DOC, 'incorrect redirect type'); 56 | chai.assert.equal(operationObj[0].value.stringValue, helper.operations.ROUTE, 'incorrect operation'); 57 | chai.assert.equal(pathObj[0].value.stringValue, '/', 'not logging old path correctly'); 58 | done(); 59 | }, 6000); 60 | }).timeout(7000); 61 | }); 62 | 63 | after(() => sandbox.restore()); 64 | -------------------------------------------------------------------------------- /packages/web/src/instrumentation/xmlHttpInstrumentation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* eslint-disable max-len */ 3 | import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request'; 4 | 5 | const api = require('@opentelemetry/api'); 6 | const core1 = require('@opentelemetry/core'); 7 | const semanticConventions1 = require('@opentelemetry/semantic-conventions'); 8 | const diag = api.diag; 9 | 10 | class EpsagonXMLHttpRequestInstrumentation extends XMLHttpRequestInstrumentation { 11 | constructor(config, parentSpan, options) { 12 | super(config); 13 | this.epsParentSpan = parentSpan; 14 | this.globalOptions = options; 15 | } 16 | 17 | // create span copied over so parent span can be added at creation 18 | _createSpan(xhr, url, method) { 19 | diag.debug('xmlHttpInstrumentation: create span for url: ', url, 'method: ', method); 20 | if (core1.isUrlIgnored(url, this._getConfig().ignoreUrls)) { 21 | diag.debug('ignoring span as url matches ignored url'); 22 | return undefined; 23 | } 24 | const spanName = `HTTP ${method.toUpperCase()}`; 25 | const currentSpan = this.tracer.startSpan(spanName, { 26 | kind: api.SpanKind.CLIENT, 27 | attributes: { 28 | [semanticConventions1.SemanticAttributes.HTTP_METHOD]: method, 29 | [semanticConventions1.SemanticAttributes.HTTP_URL]: url, 30 | }, 31 | }, this.epsParentSpan.currentSpan ? api.trace.setSpan(api.context.active(), this.epsParentSpan.currentSpan) : undefined); 32 | currentSpan.addEvent('open'); 33 | this._cleanPreviousSpanInformation(xhr); 34 | this._xhrMem.set(xhr, { 35 | span: currentSpan, 36 | spanUrl: url, 37 | }); 38 | diag.debug('xmlHttpInstrumentation: created span: ', currentSpan); 39 | return currentSpan; 40 | } 41 | 42 | _addFinalSpanAttributes(span, xhrMem, spanUrl) { 43 | diag.debug('xmlHttpInstrumentation: before add final attributes: ', span); 44 | super._addFinalSpanAttributes(span, xhrMem, spanUrl); 45 | let responseBody = xhrMem.xhrInstance.response; 46 | 47 | if (typeof spanUrl === 'string' && !this.globalOptions.metadataOnly) { 48 | responseBody = typeof responseBody !== 'string' ? JSON.stringify(responseBody) : responseBody; 49 | span.setAttribute('http.response.body', responseBody.substring(0, 5000)); 50 | const resHeadersArr = xhrMem.xhrInstance.getAllResponseHeaders().split('\r\n'); 51 | if (resHeadersArr.length > 0) { 52 | const headersObj = resHeadersArr.reduce((acc, current) => { 53 | const parts = current.split(': '); 54 | const key = parts[0]; 55 | const value = parts[1]; 56 | acc[key] = value; 57 | return acc; 58 | }, {}); 59 | span.setAttribute('http.response.headers', `${JSON.stringify(headersObj)}`); 60 | } 61 | span.setAttribute('http.response_content_length', xhrMem.xhrInstance.getResponseHeader('content-length')); 62 | span.setAttribute('http.request.body', xhrMem.xhrInstance.__zone_symbol__xhrTask.data.args[0]); 63 | diag.debug('xmlHttpInstrumentation: after add final attributes: ', span); 64 | } 65 | } 66 | 67 | // adds xhr to the xhr mem so we can parse response in final span attributes 68 | _addResourceObserver(xhr, spanUrl) { 69 | super._addResourceObserver(xhr, spanUrl); 70 | const xhrMem = this._xhrMem.get(xhr); 71 | if (!xhrMem) { 72 | return; 73 | } 74 | xhrMem.xhrInstance = xhr; 75 | } 76 | } 77 | 78 | export default EpsagonXMLHttpRequestInstrumentation; 79 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dev@epsagon.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /packages/web/src/utils.js: -------------------------------------------------------------------------------- 1 | class EpsagonUtils { 2 | static addEpsSpanAttrs(span, parentSpan) { 3 | if (parentSpan.identifyFields) { 4 | const { 5 | userId, userName, userEmail, companyId, companyName, 6 | } = parentSpan.identifyFields; 7 | if (userId) span.setAttribute('user.id', parentSpan.identifyFields.userId); 8 | if (userName) span.setAttribute('user.name', parentSpan.identifyFields.userName); 9 | if (userEmail) span.setAttribute('user.email', parentSpan.identifyFields.userEmail); 10 | if (companyId) span.setAttribute('company.id', parentSpan.identifyFields.companyId); 11 | if (companyName) span.setAttribute('company.name', parentSpan.identifyFields.companyName); 12 | } 13 | if (parentSpan.tags) { 14 | Object.keys(parentSpan.tags).forEach((key) => { 15 | span.setAttribute(key, parentSpan.tags[key]); 16 | }); 17 | } 18 | } 19 | 20 | static parseURL(httpUrl, span, _spanAttributes, _attributesLength, metadataOnly) { 21 | let attributesLength = _attributesLength; 22 | const spanAttributes = _spanAttributes; 23 | 24 | if (httpUrl.indexOf('?') < 0 && httpUrl.indexOf(';') < 0) { 25 | const path = httpUrl.substring(httpUrl.indexOf(span.name) + span.name.length); 26 | spanAttributes[attributesLength] = { key: 'http.request.path', value: { stringValue: path } }; 27 | attributesLength += 1; 28 | } 29 | if (httpUrl.indexOf('?') > 0) { 30 | const path = httpUrl.substring(httpUrl.indexOf(span.name) + span.name.length, httpUrl.indexOf('?')); 31 | spanAttributes[attributesLength] = { key: 'http.request.path', value: { stringValue: path } }; 32 | attributesLength += 1; 33 | if (!metadataOnly) { 34 | const query = httpUrl.substring(httpUrl.indexOf('?')); 35 | spanAttributes[attributesLength] = { key: 'http.request.query', value: { stringValue: query } }; 36 | attributesLength += 1; 37 | } 38 | } 39 | if (httpUrl.indexOf(';') > 0) { 40 | const path = httpUrl.substring(httpUrl.indexOf(span.name) + span.name.length, httpUrl.indexOf(';')); 41 | spanAttributes[attributesLength] = { key: 'http.request.path', value: { stringValue: path } }; 42 | attributesLength += 1; 43 | if (!metadataOnly) { 44 | const params = httpUrl.substring(httpUrl.indexOf(';')); 45 | spanAttributes[attributesLength] = { key: 'http.request.path_params', value: { stringValue: params } }; 46 | attributesLength += 1; 47 | } 48 | } 49 | 50 | return { 51 | attributesLength, 52 | spanAttributes, 53 | }; 54 | } 55 | 56 | static getFirstAttribute(span) { 57 | if (span && span.attributes && span.attributes.length) { 58 | return span.attributes[0]; 59 | } 60 | return null; 61 | } 62 | 63 | static getFirstResourceSpan(span) { 64 | if (span && span.resourceSpans && span.resourceSpans.length) { 65 | return span.resourceSpans[0]; 66 | } 67 | return null; 68 | } 69 | 70 | static genErrorAttribution(error) { 71 | return [ 72 | this.verifyAttributionStringValue('exception.message', error.message, 'exception'), 73 | this.verifyAttributionStringValue('exception.type', error.type, 'exception'), 74 | this.verifyAttributionStringValue('exception.stacktrace', error.stack, 'exception'), 75 | ]; 76 | } 77 | 78 | static verifyAttributionStringValue(keyValue, value, defaultValue) { 79 | if (value && value.length > 0) { 80 | return { key: keyValue, value: { stringValue: value } }; 81 | } 82 | return { key: keyValue, value: { stringValue: defaultValue } }; 83 | } 84 | } 85 | 86 | export default EpsagonUtils; 87 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@epsagon/react", 3 | "version": "0.0.0-development", 4 | "description": "This package provides tracing to React web applications for the collection of distributed tracing and performance metrics.", 5 | "author": "Epsagon Team ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/epsagon/epsagon-browser.git" 10 | }, 11 | "keywords": [ 12 | "browser tracing", 13 | "epsagon", 14 | "react", 15 | "tracing", 16 | "distributed-tracing", 17 | "real user monitoring", 18 | "client side monitoring", 19 | "debugging", 20 | "monitoring" 21 | ], 22 | "main": "dist/index.js", 23 | "module": "dist/index.modern.js", 24 | "source": "src/index.js", 25 | "types": "dist/index.d.ts", 26 | "engines": { 27 | "node": ">=10" 28 | }, 29 | "babel": { 30 | "presets": [ 31 | [ 32 | "@babel/preset-env", 33 | { 34 | "targets": { 35 | "esmodules": true 36 | } 37 | } 38 | ], 39 | "@babel/preset-react", 40 | "@babel/preset-flow" 41 | ], 42 | "plugins": [ 43 | "@babel/plugin-proposal-class-properties" 44 | ] 45 | }, 46 | "scripts": { 47 | "build": "microbundle-crl --no-compress --format modern,cjs", 48 | "start": "microbundle-crl watch --no-compress --format modern,cjs", 49 | "prepare": "npx tsc && run-s build", 50 | "build-dist": "./node_modules/typescript/bin/tsc", 51 | "test": "run-s test:unit test:build", 52 | "test:build": "run-s build", 53 | "test:lint": "eslint .", 54 | "test:unit": "cross-env CI=1 react-scripts test --env=jsdom", 55 | "test:watch": "react-scripts test --env=jsdom", 56 | "predeploy": "cd example && npm install && npm run build", 57 | "deploy": "gh-pages -d example/build", 58 | "semantic-release": "semantic-release" 59 | }, 60 | "peerDependencies": { 61 | "react": "^16.0.0" 62 | }, 63 | "dependencies": { 64 | "@epsagon/web": "^1.1.5", 65 | "@opentelemetry/plugin-react-load": "^0.16.0", 66 | "ua-parser-js": "^0.7.28" 67 | }, 68 | "devDependencies": { 69 | "@babel/plugin-proposal-class-properties": "^7.13.0", 70 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 71 | "@babel/plugin-transform-react-jsx": "^7.14.3", 72 | "@babel/preset-env": "^7.14.4", 73 | "@babel/preset-flow": "^7.13.13", 74 | "@babel/preset-react": "^7.13.13", 75 | "@babel/runtime": "^7.14.0", 76 | "babel-eslint": "^10.0.3", 77 | "cross-env": "^7.0.2", 78 | "eslint-config-airbnb-base": "^14.2.1", 79 | "eslint-config-prettier": "^6.7.0", 80 | "eslint-config-standard": "^14.1.0", 81 | "eslint-config-standard-react": "^9.2.0", 82 | "eslint-plugin-import": "^2.23.4", 83 | "eslint-plugin-node": "^11.0.0", 84 | "eslint-plugin-prettier": "^3.1.1", 85 | "eslint-plugin-promise": "^4.2.1", 86 | "eslint-plugin-react": "^7.17.0", 87 | "eslint-plugin-standard": "^4.0.1", 88 | "gh-pages": "^2.2.0", 89 | "microbundle-crl": "^0.13.10", 90 | "npm-run-all": "^4.1.5", 91 | "prettier": "^2.0.4", 92 | "typescript": "^4.3.4", 93 | "react-scripts": "^4.0.3", 94 | "semantic-release": "^17.4.4" 95 | }, 96 | "release": { 97 | "branches": [ 98 | "main" 99 | ] 100 | }, 101 | "publishConfig": { 102 | "access": "public" 103 | }, 104 | "bugs": { 105 | "url": "https://github.com/epsagon/epsagon-browser/issues" 106 | }, 107 | "homepage": "https://github.com/epsagon/epsagon-browser#readme", 108 | "files": [ 109 | "dist" 110 | ] 111 | } 112 | -------------------------------------------------------------------------------- /packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@epsagon/web", 3 | "version": "0.0.0-development", 4 | "description": "This package provides tracing to Node.js web applications for the collection of distributed tracing and performance metrics.", 5 | "author": "Epsagon Team ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/epsagon/epsagon-browser.git" 10 | }, 11 | "keywords": [ 12 | "browser tracing", 13 | "epsagon", 14 | "tracing", 15 | "distributed-tracing", 16 | "real user monitoring", 17 | "client side monitoring", 18 | "debugging", 19 | "monitoring" 20 | ], 21 | "main": "dist/index.js", 22 | "module": "dist/index.modern.js", 23 | "source": "src/index.js", 24 | "types": "dist/index.d.ts", 25 | "engines": { 26 | "node": ">=10" 27 | }, 28 | "babel": { 29 | "presets": [ 30 | [ 31 | "@babel/preset-env", 32 | { 33 | "targets": { 34 | "esmodules": true 35 | } 36 | } 37 | ], 38 | "@babel/preset-react", 39 | "@babel/preset-flow" 40 | ], 41 | "plugins": [ 42 | "@babel/plugin-proposal-class-properties" 43 | ] 44 | }, 45 | "scripts": { 46 | "build": "microbundle-crl --no-compress --format modern,cjs; webpack", 47 | "start": "microbundle-crl watch --no-compress --format modern,cjs", 48 | "prepare": "npx tsc && run-s build", 49 | "build-dist": "./node_modules/typescript/bin/tsc", 50 | "test:build": "run-s build", 51 | "test:lint": "eslint .", 52 | "test:lintfix": "eslint . --fix", 53 | "test:unit": "cross-env CI=1 mocha --require @babel/register", 54 | "test:watch": "mocha --require @babel/register --require mocha-suppress-logs --exit", 55 | "test": "mocha --require @babel/register --require mocha-suppress-logs --exit", 56 | "predeploy": "cd example && npm install && npm run build", 57 | "deploy": "gh-pages -d example/build", 58 | "semantic-release": "semantic-release" 59 | }, 60 | "peerDependencies": { 61 | "react": "^16.0.0" 62 | }, 63 | "dependencies": { 64 | "@opentelemetry/api": "^0.20.0", 65 | "@opentelemetry/context-zone": "^0.20.0", 66 | "@opentelemetry/core": "^0.20.0", 67 | "@opentelemetry/exporter-collector": "^0.20.0", 68 | "@opentelemetry/instrumentation": "^0.20.0", 69 | "@opentelemetry/instrumentation-document-load": "^0.20.0", 70 | "@opentelemetry/instrumentation-fetch": "^0.20.0", 71 | "@opentelemetry/instrumentation-user-interaction": "^0.16.0", 72 | "@opentelemetry/instrumentation-xml-http-request": "^0.20.0", 73 | "@opentelemetry/semantic-conventions": "^0.23.0", 74 | "@opentelemetry/tracing": "^0.20.0", 75 | "@opentelemetry/web": "^0.20.0", 76 | "node-fetch": "^2.6.1", 77 | "ua-parser-js": "^0.7.28" 78 | }, 79 | 80 | "devDependencies": { 81 | "@babel/plugin-proposal-class-properties": "^7.13.0", 82 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 83 | "@babel/plugin-transform-react-jsx": "^7.14.3", 84 | "@babel/preset-env": "^7.14.4", 85 | "@babel/preset-flow": "^7.13.13", 86 | "@babel/preset-react": "^7.13.13", 87 | "@babel/register": "^7.14.5", 88 | "@babel/runtime": "^7.14.0", 89 | "babel-eslint": "^10.0.3", 90 | "chai": "^4.3.4", 91 | "cross-env": "^7.0.2", 92 | "eslint-config-airbnb-base": "^14.2.1", 93 | "eslint-config-prettier": "^6.7.0", 94 | "eslint-config-standard": "^14.1.0", 95 | "eslint-config-standard-react": "^9.2.0", 96 | "eslint-plugin-import": "^2.23.4", 97 | "eslint-plugin-node": "^11.0.0", 98 | "eslint-plugin-prettier": "^3.1.1", 99 | "eslint-plugin-promise": "^4.2.1", 100 | "eslint-plugin-react": "^7.17.0", 101 | "eslint-plugin-standard": "^4.0.1", 102 | "gh-pages": "^3.2.3", 103 | "microbundle-crl": "^0.13.10", 104 | "mocha": "^9.0.2", 105 | "mocha-suppress-logs": "^0.3.1", 106 | "npm-run-all": "^4.1.5", 107 | "npx": "^10.2.2", 108 | "prettier": "^2.0.4", 109 | "react": "^16.13.1", 110 | "react-dom": "^16.13.1", 111 | "react-scripts": "^4.0.3", 112 | "semantic-release": "^17.4.4", 113 | "sinon": "^10.0.0", 114 | "typescript": "^4.3.4", 115 | "webpack-cli": "^4.8.0" 116 | }, 117 | "release": { 118 | "branches": [ 119 | "main" 120 | ] 121 | }, 122 | "publishConfig": { 123 | "access": "public" 124 | }, 125 | "bugs": { 126 | "url": "https://github.com/epsagon/epsagon-browser/issues" 127 | }, 128 | "homepage": "https://github.com/epsagon/epsagon-browser#readme", 129 | "files": [ 130 | "dist" 131 | ] 132 | } 133 | -------------------------------------------------------------------------------- /packages/web/src/instrumentation/documentLoadInstrumentation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* eslint-disable max-len */ 3 | import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load'; 4 | import { diag } from '@opentelemetry/api'; 5 | import EpsagonUtils from '../utils'; 6 | 7 | const api = require('@opentelemetry/api'); 8 | 9 | class EpsagonDocumentLoadInstrumentation extends DocumentLoadInstrumentation { 10 | constructor(parentSpan, isErrorCollectionDisabled) { 11 | super(); 12 | this.epsParentSpan = parentSpan; 13 | this.isErrorCollectionDisabled = isErrorCollectionDisabled; 14 | } 15 | 16 | _onDocumentLoaded(event = false) { 17 | // Timeout is needed as load event doesn't have yet the performance metrics for loadEnd. 18 | // Support for event "loadend" is very limited and cannot be used 19 | /* eslint-disable no-undef */ 20 | window.setTimeout(() => { 21 | if ((event.error || event.reason) && !this.isErrorCollectionDisabled) { 22 | // if (event.error || event.reason) { 23 | this.reportError(event); 24 | } else { 25 | this._collectPerformance(); 26 | } 27 | }); 28 | } 29 | 30 | _startSpan(spanName, performanceName, entries) { 31 | diag.debug('start span with name: ', spanName); 32 | // drop document fetch events 33 | if (spanName === 'documentFetch') { 34 | return undefined; 35 | } 36 | const initialSpan = super._startSpan(spanName, performanceName, entries, this.epsParentSpan.currentSpan); 37 | if (initialSpan && !this.epsParentSpan.currentSpan) { 38 | this.epsParentSpan.currentSpan = initialSpan; 39 | } 40 | EpsagonUtils.addEpsSpanAttrs(initialSpan, this.epsParentSpan); 41 | diag.debug('initialSpan: ', initialSpan); 42 | return initialSpan; 43 | } 44 | 45 | // drop resource fetch spans 46 | /* eslint-disable class-methods-use-this */ 47 | _initResourceSpan() { 48 | } 49 | 50 | /* eslint-disable class-methods-use-this */ 51 | _includes(obj, str) { 52 | if (!obj) { 53 | return false; 54 | } 55 | if (typeof obj === 'string' || obj instanceof Array) { 56 | return obj.indexOf(str) !== -1; 57 | } 58 | return false; 59 | } 60 | 61 | reportError(event) { 62 | diag.debug('DocumentLoadInstrumentation: reportError'); 63 | const error = event.error ? event.error : event.reason; 64 | const failedToExportError = (this._includes(error.message, 'Failed to export with XHR (status: 502)')) || this._includes(error, 'Failed to export with XHR (status: 502)'); 65 | if (error && failedToExportError) { 66 | return; 67 | } 68 | const span = this.tracer.startSpan('error', { 69 | attributes: { 70 | message: error.message || error, 71 | type: 'browser', 72 | operation: 'page_load', 73 | }, 74 | }, this.epsParentSpan.currentSpan ? api.trace.setSpan(api.context.active(), this.epsParentSpan.currentSpan) : undefined); 75 | span.exceptionData = { 76 | name: 'exception', 77 | attributes: EpsagonUtils.genErrorAttribution(error), 78 | }; 79 | EpsagonUtils.addEpsSpanAttrs(span, this.epsParentSpan); 80 | span.setStatus({ code: 2 }); 81 | diag.debug('error span: ', span); 82 | span.end(); 83 | } 84 | 85 | /* eslint-disable no-undef */ 86 | _waitForPageLoad() { 87 | if (window.document.readyState === 'complete') { 88 | this._onDocumentLoaded(); 89 | } else { 90 | this._onDocumentLoaded = this._onDocumentLoaded.bind(this); 91 | window.addEventListener('load', this._onDocumentLoaded); 92 | window.addEventListener('error', this._onDocumentLoaded); 93 | window.addEventListener('unhandledrejection', this._onDocumentLoaded); 94 | window.addEventListener('rejectionhandled', this._onDocumentLoaded); 95 | } 96 | } 97 | 98 | /* eslint-disable no-undef */ 99 | enable() { 100 | // remove previously attached load to avoid adding the same event twice 101 | // in case of multiple enable calling. 102 | window.removeEventListener('load', this._onDocumentLoaded); 103 | window.removeEventListener('error', this._onDocumentLoaded); 104 | window.removeEventListener('unhandledrejection', this._onDocumentLoaded); 105 | window.removeEventListener('rejectionhandled', this._onDocumentLoaded); 106 | this._waitForPageLoad(); 107 | } 108 | 109 | /* eslint-disable no-undef */ 110 | disable() { 111 | super.disable(); 112 | window.removeEventListener('error', this._onDocumentLoaded); 113 | window.removeEventListener('unhandledrejection', this._onDocumentLoaded); 114 | window.removeEventListener('rejectionhandled', this._onDocumentLoaded); 115 | } 116 | } 117 | 118 | export default EpsagonDocumentLoadInstrumentation; 119 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |
6 |

7 | 8 | # Deprecated - please use epsagon/web instead 9 | 10 | # Epsagon Tracing for Web 11 | 12 | This package provides tracing to front end of web applications for the collection of distributed tracing and performance metrics in [Epsagon](https://app.epsagon.com/?utm_source=github). 13 | 14 | ## Contents 15 | 16 | - [Installation](#installation) 17 | - [Usage](#usage) 18 | - [Custom Tags](#custom-tags) 19 | - [Configuration](#configuration) 20 | - [Trace Header Propagation](#trace-header-propagation) 21 | - [Getting Help](#getting-help) 22 | - [Opening Issues](#opening-issues) 23 | - [License](#license) 24 | 25 | ## Installation 26 | 27 | To install Epsagon, simply run: 28 | ```sh 29 | npm install @epsagon/react 30 | ``` 31 | 32 | ## Usage 33 | 34 | To initialize the tracer, import the SDK and call the init function before the start of your project. 35 | 36 | ```javascript 37 | import epsagon from '@epsagon/react' 38 | 39 | epsagon.init({ 40 | token: 'epsagon-token', 41 | appName: 'app-name-stage', 42 | }) 43 | ``` 44 | 45 | ## Custom Tags 46 | 47 | To add additional information to spans there are two methods available. Batch add user identity information with the ```epsagon.identity``` function, or use the ```epsagon.tag``` function to add your own custom information. 48 | 49 | Options for ```epsagon.identify``` include { userId, userName, userEmail, companyId, companyName }. 50 | 51 | ```js 52 | epsagon.identify({ 53 | userId: '7128f1a08a95e46c', 54 | userName: 'John Doe', 55 | userEmail: 'john@doe.com', 56 | companyId: 'fcffa7328813e4', 57 | companyName: 'Epsagon' 58 | }) 59 | 60 | ``` 61 | 62 | Custom tags can only be added one at a time by passing a key and value to the tag function. 63 | 64 | ```js 65 | epsagon.tag('PurchaseId', '2ef5b4bfdd') 66 | ``` 67 | 68 | ## Configuration 69 | 70 | Advanced options can be configured as a parameter to the init() method. 71 | 72 | |Parameter |Type |Default |Description | 73 | |-------------------|-------|-------------|-----------------------------------------------------------------------------------| 74 | |token |String |- |Epsagon account token | 75 | |appName |String |`Epsagon Application`|Application name that will be set for traces | 76 | |collectorURL |String |-|The address of the trace collector to send trace to | 77 | |metadataOnly |Boolean|`false` |Whether to send only the metadata (`true`) or also the payloads (`false`) | 78 | |propagateTraceHeaderUrls |Array|`*` |Which outgoing requests to add traceparent headers to. Defaults to all. | 79 | |isEpsagonDisabled |Boolean|`false` |A flag to completely disable Epsagon (can be used for tests or locally) | 80 | 81 | 82 | ### Trace Header Propagation 83 | By default all outgoing requests will be added with a `traceparent` header which allows Epsagon to connect the front end trace to the backend traces. Some external services will not accept a traceparent header on request. If you need to limit the traceparent headers to requests to internal services, pass in an array of the hosts you do want to connect to in the propagateTraceHeaderUrls param in the config. 84 | 85 | ```javascript 86 | import epsagon from '@epsagon/react' 87 | 88 | epsagon.init({ 89 | token: 'epsagon-token', 90 | appName: 'app-name-stage', 91 | propagateTraceHeaderUrls: ['localhost', 'sub.example.com'] 92 | }) 93 | ``` 94 | 95 | ## Getting Help 96 | 97 | If you have any issue around using the library or the product, please don't hesitate to: 98 | 99 | * Use the [documentation](https://docs.epsagon.com). 100 | * Use the help widget inside the product. 101 | * Open an issue in GitHub. 102 | 103 | 104 | ## Opening Issues 105 | 106 | If you encounter a bug with the Epsagon library, we want to hear about it. 107 | 108 | When opening a new issue, please provide as much information about the environment: 109 | * Library version, Node.js runtime version, dependencies, etc. 110 | * Snippet of the usage. 111 | * A reproducible example can really help. 112 | 113 | The GitHub issues are intended for bug reports and feature requests. 114 | For help and questions about Epsagon, use the help widget inside the product. 115 | 116 | 117 | ## License 118 | 119 | Provided under the MIT license. See LICENSE for details. 120 | 121 | Copyright 2021, Epsagon 122 | 123 | -------------------------------------------------------------------------------- /packages/web/README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | 5 | 6 |
7 |

8 | 9 | # Epsagon Tracing for Web 10 | 11 | This package provides tracing to front end of web applications for the collection of distributed tracing and performance metrics in [Epsagon](https://app.epsagon.com/?utm_source=github). 12 | 13 | ## Contents 14 | 15 | - [Epsagon Tracing for Web](#epsagon-tracing-for-web) 16 | - [Contents](#contents) 17 | - [Installation](#installation) 18 | - [Usage](#usage) 19 | - [Custom Tags](#custom-tags) 20 | - [Configuration](#configuration) 21 | - [Trace Header Propagation](#trace-header-propagation) 22 | - [Getting Help](#getting-help) 23 | - [Opening Issues](#opening-issues) 24 | - [License](#license) 25 | 26 | ## Installation 27 | 28 | To install Epsagon, simply run: 29 | ```sh 30 | npm install @epsagon/web 31 | ``` 32 | 33 | ## Usage 34 | 35 | To initialize the tracer, import the SDK and call the init function before the start of your project. 36 | 37 | ```javascript 38 | import epsagon from '@epsagon/web' 39 | 40 | epsagon.init({ 41 | token: 'epsagon-token', 42 | appName: 'app-name-stage', 43 | }) 44 | ``` 45 | 46 | ## Custom Tags 47 | 48 | To add additional information to spans there are two methods available. Batch add user identity information with the ```epsagon.identity``` function, or use the ```epsagon.tag``` function to add your own custom information. 49 | 50 | Options for ```epsagon.identify``` include { userId, userName, userEmail, companyId, companyName }. 51 | 52 | ```js 53 | epsagon.identify({ 54 | userId: '7128f1a08a95e46c', 55 | userName: 'John Doe', 56 | userEmail: 'john@doe.com', 57 | companyId: 'fcffa7328813e4', 58 | companyName: 'Epsagon' 59 | }) 60 | 61 | ``` 62 | 63 | Custom tags can only be added one at a time by passing a key and value to the tag function. 64 | 65 | ```js 66 | epsagon.tag('PurchaseId', '2ef5b4bfdd') 67 | ``` 68 | 69 | ## Configuration 70 | 71 | Advanced options can be configured as a parameter to the init() method. 72 | 73 | |Parameter |Type |Default |Description | 74 | |-------------------|-------|-------------|-----------------------------------------------------------------------------------| 75 | |token |String |- |Epsagon account token | 76 | |appName |String |`Epsagon Application`|Application name that will be set for traces | 77 | |collectorURL |String |-|The address of the trace collector to send trace to | 78 | |metadataOnly |Boolean|`false` |Whether to send only the metadata (`true`) or also the payloads (`false`) | 79 | |propagateTraceHeaderUrls |Array|`*` |Which outgoing requests to add traceparent headers to. Defaults to all. | 80 | |urlPatternsToIgnore |Array|[] |Which outgoing requests to ignore (and not add traceparent to. Default to [] | 81 | |isEpsagonDisabled |Boolean|`false` |A flag to completely disable Epsagon (can be used for tests or locally) | 82 | |epsagonDebug |Boolean|`false` |Enable debug prints for troubleshooting. Note: if this flag is true, this will override the logLevel| 83 | |logLevel |String|`INFO` |The default Log level. Could be one of: ```DEBUG```, ```INFO```, ```WARN```, ```ERROR```, ```ALL```.| 84 | |errorDisabled |Boolean|`false` |Disables collection of errors while still sending traces | 85 | 86 | 87 | ### Trace Header Propagation 88 | By default all outgoing requests will be added with a `traceparent` header which allows Epsagon to connect the front end trace to the backend traces. Some external services will not accept a traceparent header on request. If you need to limit the traceparent headers to requests to internal services, pass in an array of the hosts you do want to connect to in the propagateTraceHeaderUrls param in the config. 89 | 90 | ```javascript 91 | import epsagon from '@epsagon/web' 92 | 93 | epsagon.init({ 94 | token: 'epsagon-token', 95 | appName: 'app-name-stage', 96 | propagateTraceHeaderUrls: ['localhost', 'sub.example.com'], 97 | urlPatternsToIgnore: [".*example.*", ".*abc.*"] 98 | }) 99 | ``` 100 | 101 | ## Getting Help 102 | 103 | If you have any issue around using the library or the product, please don't hesitate to: 104 | 105 | * Use the [documentation](https://docs.epsagon.com). 106 | * Use the help widget inside the product. 107 | * Open an issue in GitHub. 108 | 109 | 110 | ## Opening Issues 111 | 112 | If you encounter a bug with the Epsagon library, we want to hear about it. 113 | 114 | When opening a new issue, please provide as much information about the environment: 115 | * Library version, Node.js runtime version, dependencies, etc. 116 | * Snippet of the usage. 117 | * A reproducible example can really help. 118 | 119 | The GitHub issues are intended for bug reports and feature requests. 120 | For help and questions about Epsagon, use the help widget inside the product. 121 | 122 | 123 | ## License 124 | 125 | Provided under the MIT license. See LICENSE for details. 126 | 127 | Copyright 2021, Epsagon 128 | -------------------------------------------------------------------------------- /packages/web/test/requests.test.js: -------------------------------------------------------------------------------- 1 | import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request'; 2 | import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch/build/src/fetch'; 3 | import EpsagonExporter from '../src/exporter'; 4 | import EpsagonXMLHttpRequestInstrumentation from '../src/instrumentation/xmlHttpInstrumentation'; 5 | import EpsagonFetchInstrumentation from '../src/instrumentation/fetchInstrumentation'; 6 | 7 | const chai = require('chai'); 8 | const sinon = require('sinon'); 9 | const helper = require('./helper'); 10 | const epsagon = require('../src/web-tracer'); 11 | 12 | helper.browserenv(); 13 | const sandbox = sinon.createSandbox(); 14 | 15 | let spyCreateSpan; 16 | let spyExporter; 17 | let spyHeaders; 18 | let spyAttrs; 19 | 20 | describe('xhr instrumentation', () => { 21 | beforeEach(() => { 22 | Object.defineProperty(global.window.document, 'readyState', { 23 | writable: true, 24 | value: 'complete', 25 | }); 26 | spyHeaders = sandbox.stub(XMLHttpRequestInstrumentation.prototype, '_addHeaders'); 27 | spyHeaders.returns(null); 28 | spyAttrs = sandbox.stub(EpsagonXMLHttpRequestInstrumentation.prototype, '_addFinalSpanAttributes'); 29 | spyExporter = sandbox.spy(EpsagonExporter.prototype, 'convert'); 30 | }); 31 | 32 | afterEach(() => { 33 | spyExporter.restore(); 34 | spyHeaders.restore(); 35 | spyAttrs.restore(); 36 | }); 37 | 38 | it('should construct an instance', (done) => { 39 | const plugin = new EpsagonXMLHttpRequestInstrumentation({ 40 | enabled: false, 41 | }); 42 | chai.assert.ok(plugin instanceof EpsagonXMLHttpRequestInstrumentation); 43 | done(); 44 | }); 45 | 46 | it('should create span for xml request', (done) => { 47 | const xhr = new XMLHttpRequest(); 48 | xhr.open('POST', 'https://jsonplaceholder.typicode.com/photos/', true); 49 | xhr.send('"test": "1"'); 50 | xhr.abort(); 51 | setTimeout(() => { 52 | const spans = spyExporter.returnValues[0]; 53 | chai.assert.ok(spans.resourceSpans[0].instrumentationLibrarySpans, 'spans not created'); 54 | const span = spans.resourceSpans[0].instrumentationLibrarySpans.filter((obj) => obj.instrumentationLibrary.name == '@opentelemetry/instrumentation-xml-http-request'); 55 | chai.assert.equal(span[0].spans.length, 1, 'more then one span being created'); 56 | chai.assert.ok(span[0].spans[0].parentSpanId, 'parent span not being set'); 57 | done(); 58 | }, 7000); 59 | }).timeout(8000); 60 | }); 61 | 62 | describe('fetch instrumentation', () => { 63 | beforeEach(() => { 64 | Object.defineProperty(global.window.document, 'readyState', { 65 | writable: true, 66 | value: 'complete', 67 | }); 68 | spyHeaders = sandbox.stub(FetchInstrumentation.prototype, '_addHeaders'); 69 | spyAttrs = sandbox.stub(FetchInstrumentation.prototype, '_addFinalSpanAttributes'); 70 | spyExporter = sandbox.spy(EpsagonExporter.prototype, 'convert'); 71 | }); 72 | 73 | afterEach(() => { 74 | spyExporter.restore(); 75 | spyHeaders.restore(); 76 | spyAttrs.restore(); 77 | }); 78 | 79 | it('should create span for fetch request', (done) => { 80 | window.fetch('https://jsonplaceholder.typicode.com/photos/').then(() => { 81 | setTimeout(() => { 82 | const spans = spyExporter.returnValues[0]; 83 | chai.assert.ok(spans.resourceSpans[0].instrumentationLibrarySpans, 'spans not created'); 84 | const span = spans.resourceSpans[0].instrumentationLibrarySpans.filter((obj) => obj.instrumentationLibrary.name == '@opentelemetry/instrumentation-fetch'); 85 | chai.assert.equal(span[0].spans.length, 1, 'more then one span being created'); 86 | chai.assert.ok(span[0].spans[0].parentSpanId, 'parent span not being set'); 87 | done(); 88 | }, 6000); 89 | }); 90 | }).timeout(7000); 91 | }); 92 | 93 | describe('fetch instrumentation - urlPatternsToIgnore', () => { 94 | 95 | beforeEach(() => { 96 | Object.defineProperty(global.window.document, 'readyState', { 97 | writable: true, 98 | value: 'complete', 99 | }); 100 | spyHeaders = sandbox.stub(FetchInstrumentation.prototype, '_addHeaders'); 101 | spyCreateSpan = sandbox.spy(EpsagonFetchInstrumentation.prototype, '_createSpan'); 102 | }); 103 | 104 | afterEach(() => { 105 | spyHeaders.restore(); 106 | spyCreateSpan.restore(); 107 | }); 108 | 109 | it('should ignore creating span according to the given urlPatternsToIgnore', (done) => { 110 | epsagon.init({ token: 'sdfsdfdf', appName: 'test app', isTest: true, urlPatternsToIgnore: [".*place.*"] }); 111 | window.fetch('https://jsonplaceholder.typicode.com/photos/').then(() => { 112 | setTimeout(() => { 113 | chai.assert.notExists(spyCreateSpan.returnValues[0]); 114 | done(); 115 | }, 6000); 116 | }); 117 | }).timeout(7000); 118 | 119 | it('should create a span according to the given urlPatternsToIgnore', (done) => { 120 | epsagon.init({ token: 'sdasdf', appName: 'test app', isTest: true, urlPatternsToIgnore: [".*abc.*"] }); 121 | window.fetch('https://jsonplaceholder.typicode.com/photos/').then(() => { 122 | setTimeout(() => { 123 | chai.assert.ok(spyCreateSpan.returnValues[0], "span wasn't created"); 124 | done(); 125 | }, 6000); 126 | }); 127 | }).timeout(7000); 128 | }); 129 | 130 | after(() => sandbox.restore()); 131 | -------------------------------------------------------------------------------- /packages/web/test/web-tracer.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const epsagon = require('../src/web-tracer'); 3 | import {diag, DiagLogLevel} from '@opentelemetry/api'; 4 | import * as sinon from 'sinon'; 5 | const helper = require('./helper'); 6 | 7 | before(helper.browserenv); 8 | const appName = 'test app'; 9 | 10 | describe('init tests', () => { 11 | const sandbox = sinon.createSandbox(); 12 | 13 | afterEach(function() { 14 | sandbox.restore(); 15 | }); 16 | 17 | it('init function exists', (done) => { 18 | chai.expect(typeof epsagon.init === 'function').to.equal(true); 19 | done(); 20 | }); 21 | 22 | it('init function produces tracer and epsSpan', (done) => { 23 | const res = epsagon.init({ token: 'fasdfsafa', appName, isTest: true }); 24 | chai.assert.exists(res.tracer, 'tracer was created'); 25 | chai.assert.exists(res.epsSpan, 'epsSpan was created'); 26 | chai.assert.equal(res.tracer.instrumentationLibrary.name, appName, 'app name should be passed into tracer'); 27 | chai.assert.exists(res.epsSpan.currentSpan, 'current span should have been created'); 28 | done(); 29 | }); 30 | 31 | it('init function returns if no token passed in', (done) => { 32 | const res = epsagon.init({ appName, isTest: true }); 33 | chai.assert.notExists(res, 'res should be false'); 34 | done(); 35 | }); 36 | 37 | it('init function returns if epsagon disabled', (done) => { 38 | const res = epsagon.init({ 39 | token: 'fasdfsafa', appName, isEpsagonDisabled: true, isTest: true, 40 | }); 41 | chai.assert.notExists(res, 'res should be false'); 42 | done(); 43 | }); 44 | 45 | it('init function produces tracer and epsSpan even if epsagon is not sampled', (done) => { 46 | const res = epsagon.init({ 47 | token: 'fasdfsafa', appName, isTest: true, networkSamplingRatio: 0, 48 | }); 49 | chai.assert.exists(res.tracer, 'tracer was created'); 50 | chai.assert.exists(res.epsSpan, 'epsSpan was created'); 51 | chai.assert.equal(res.tracer.instrumentationLibrary.name, appName, 'app name should be passed into tracer'); 52 | chai.assert.exists(res.epsSpan.currentSpan, 'current span should have been created'); 53 | done(); 54 | }); 55 | }); 56 | 57 | describe('logging tests', () => { 58 | const createLoggerStub = sinon.fake(); 59 | beforeEach(() => { 60 | diag.setLogger = createLoggerStub 61 | }); 62 | 63 | afterEach(() => { 64 | createLoggerStub.resetHistory() 65 | }); 66 | 67 | 68 | it('default debug false test', (done) => { 69 | const res = epsagon.init({ token: 'fasdfsafa', appName, isTest: true, epsagonDebug: false }); 70 | chai.assert.isFalse(createLoggerStub.calledOnce) 71 | done(); 72 | }); 73 | 74 | it('default debug true test', (done) => { 75 | const res = epsagon.init({ token: 'fasdfsafa', appName, isTest: true, epsagonDebug: true }); 76 | chai.assert.isTrue(createLoggerStub.calledOnce) 77 | chai.assert.equal(createLoggerStub.lastArg, DiagLogLevel.DEBUG) 78 | done(); 79 | }); 80 | 81 | it('no log level', (done) => { 82 | const res = epsagon.init({ token: 'fasdfsafa', appName, isTest: true }); 83 | chai.assert.isFalse(createLoggerStub.calledOnce) 84 | done(); 85 | }); 86 | 87 | it('log level debug', (done) => { 88 | const res = epsagon.init({ token: 'fasdfsafa', appName: appName, isTest: true, logLevel: 'DEBUG'}); 89 | chai.assert.isTrue(createLoggerStub.calledOnce) 90 | chai.assert.equal(createLoggerStub.lastArg, DiagLogLevel.DEBUG) 91 | done(); 92 | }); 93 | 94 | it('log level info', (done) => { 95 | const res = epsagon.init({ token: 'fasdfsafa', appName: appName, isTest: true, logLevel: 'INFO'}); 96 | chai.assert.isTrue(createLoggerStub.calledOnce) 97 | chai.assert.equal(createLoggerStub.lastArg, DiagLogLevel.INFO) 98 | done(); 99 | }); 100 | 101 | it('log level warn', (done) => { 102 | const res = epsagon.init({ token: 'fasdfsafa', appName: appName, isTest: true, logLevel: 'WARN'}); 103 | chai.assert.isTrue(createLoggerStub.calledOnce) 104 | chai.assert.equal(createLoggerStub.lastArg, DiagLogLevel.WARN) 105 | done(); 106 | }); 107 | 108 | it('log level error', (done) => { 109 | const res = epsagon.init({ token: 'fasdfsafa', appName: appName, isTest: true, logLevel: 'ERROR'}); 110 | chai.assert.isTrue(createLoggerStub.calledOnce) 111 | chai.assert.equal(createLoggerStub.lastArg, DiagLogLevel.ERROR) 112 | done(); 113 | }); 114 | 115 | it('log level all', (done) => { 116 | const res = epsagon.init({ token: 'fasdfsafa', appName: appName, isTest: true, logLevel: 'ALL'}); 117 | chai.assert.isTrue(createLoggerStub.calledOnce) 118 | chai.assert.equal(createLoggerStub.lastArg, DiagLogLevel.ALL) 119 | done(); 120 | }); 121 | }); 122 | 123 | describe('tags tests', () => { 124 | it('identify adds tags to epsSpan', (done) => { 125 | const options = { 126 | userId: 'test user', 127 | userName: 'test name', 128 | userEmail: 'test email', 129 | companyId: 'company id test', 130 | companyName: 'company name', 131 | }; 132 | const res = epsagon.init({ token: 'fasdfsafa', appName, isTest: true }); 133 | chai.assert.exists(res.epsSpan, 'epsSpan was created'); 134 | chai.assert.exists(res.epsSpan.currentSpan, 'current span should have been created'); 135 | epsagon.identify(options); 136 | chai.assert.exists(res.epsSpan.identifyFields, 'identity fields should have been added to epsspan'); 137 | chai.assert.deepEqual(res.epsSpan.identifyFields, options, 'identity fields should equal passed in fields'); 138 | done(); 139 | }); 140 | 141 | it('tags adds to epsSpan', (done) => { 142 | const sampleKey = 'sample key'; 143 | const sampleValue = 'sample value'; 144 | const res = epsagon.init({ token: 'fasdfsafa', appName, isTest: true }); 145 | chai.assert.exists(res.epsSpan, 'epsSpan was created'); 146 | chai.assert.exists(res.epsSpan.currentSpan, 'current span should have been created'); 147 | epsagon.tag(sampleKey, sampleValue); 148 | chai.assert.exists(res.epsSpan.tags, 'tag should have been added to epsspan'); 149 | chai.assert.deepEqual(res.epsSpan.tags, { [sampleKey]: sampleValue }, 'tag should equal passed in fields'); 150 | done(); 151 | }); 152 | }); 153 | 154 | after(helper.restore); 155 | -------------------------------------------------------------------------------- /packages/web/test/docLoad.test.js: -------------------------------------------------------------------------------- 1 | import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load'; 2 | import EpsagonDocumentLoadInstrumentation from '../src/instrumentation/documentLoadInstrumentation'; 3 | import EpsagonExporter from '../src/exporter'; 4 | 5 | const chai = require('chai'); 6 | const sinon = require('sinon'); 7 | const helper = require('./helper'); 8 | 9 | const sandbox = sinon.createSandbox(); 10 | 11 | const entries = { 12 | connectEnd: 17.90000000037253, 13 | connectStart: 17.90000000037253, 14 | domComplete: 1131.0999999996275, 15 | domContentLoadedEventEnd: 586.6999999992549, 16 | domContentLoadedEventStart: 586.6999999992549, 17 | domInteractive: 586.6999999992549, 18 | domainLookupEnd: 17.90000000037253, 19 | domainLookupStart: 17.90000000037253, 20 | encodedBodySize: 897, 21 | fetchStart: 17.90000000037253, 22 | loadEventEnd: 1131.2999999988824, 23 | loadEventStart: 1131.0999999996275, 24 | redirectEnd: 0, 25 | redirectStart: 0, 26 | requestStart: 36.90000000037253, 27 | responseEnd: 57.40000000037253, 28 | responseStart: 55.09999999962747, 29 | secureConnectionStart: 0, 30 | unloadEventEnd: 95.69999999925494, 31 | unloadEventStart: 75.40000000037253, 32 | }; 33 | 34 | let spyEntries; 35 | let spyExporter; 36 | describe('docload instrumentation', () => { 37 | before(() => { 38 | helper.browserenv({errorDisabled: false}); 39 | }); 40 | 41 | beforeEach(() => { 42 | Object.defineProperty(global.window.document, 'readyState', { 43 | writable: true, 44 | value: 'complete', 45 | }); 46 | spyEntries = sandbox.stub(DocumentLoadInstrumentation.prototype, '_getEntries'); 47 | spyEntries.returns(entries); 48 | spyExporter = sandbox.spy(EpsagonExporter.prototype, 'convert'); 49 | }); 50 | 51 | afterEach(() => { 52 | spyEntries.restore(); 53 | spyExporter.restore(); 54 | }); 55 | 56 | it('should construct an instance', (done) => { 57 | const plugin = new EpsagonDocumentLoadInstrumentation({ 58 | enabled: false, 59 | }); 60 | chai.assert.ok(plugin instanceof EpsagonDocumentLoadInstrumentation); 61 | done(); 62 | }); 63 | 64 | it('should create span for document load', (done) => { 65 | setTimeout(() => { 66 | const spans = spyExporter.returnValues[0]; 67 | chai.assert.ok(spans.resourceSpans[0].instrumentationLibrarySpans[0], 'spans not created'); 68 | const span = spans.resourceSpans[0].instrumentationLibrarySpans[0]; 69 | chai.assert.equal(span.spans.length, 1, 'more then one doc load span being created'); 70 | chai.assert.equal(span.spans[0].name, '/', 'Span name was not converted to path name'); 71 | const typeObj = span.spans[0].attributes.filter((obj) => obj.key === 'type'); 72 | const operationObj = span.spans[0].attributes.filter((obj) => obj.key === 'operation'); 73 | chai.assert.equal(typeObj[0].value.stringValue, helper.type.DOC, 'incorrect doc load type'); 74 | chai.assert.equal(operationObj[0].value.stringValue, helper.operations.LOAD, 'incorrect doc load operation'); 75 | done(); 76 | }, 5000); 77 | }).timeout(6000); 78 | 79 | it('should add error to parent load span', (done) => { 80 | window.dispatchEvent( 81 | new window.CustomEvent('load', { 82 | bubbles: true, 83 | cancelable: false, 84 | composed: true, 85 | detail: {}, 86 | }), 87 | ); 88 | helper.createEmptyStackError(); 89 | setTimeout(() => { 90 | const spans = spyExporter.returnValues[0]; 91 | chai.assert.ok(spans.resourceSpans[0].instrumentationLibrarySpans[0], 'spans not created'); 92 | const errorSpan = spans.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; 93 | chai.assert.equal(errorSpan.name, '/', 'Span name was not converted to path name'); 94 | chai.assert.equal(errorSpan.events[0].name, 'exception', 'Error not added as event on doc load span'); 95 | const errorEvent = errorSpan.events[0]; 96 | chai.assert.equal(errorEvent.attributes.length, 3, 'error event should have 3 attributes: error message, type and stacktrace') 97 | chai.assert.deepEqual(errorEvent.attributes[0], { key: 'exception.message', value: { stringValue: 'my error' } }, 'exception didnt capture error message'); 98 | chai.assert.deepEqual(errorEvent.attributes[1], { key: 'exception.type', value: { stringValue: 'my error type' } }, 'exception didnt capture error type'); 99 | const typeObj = errorSpan.attributes.filter((obj) => obj.key === 'type'); 100 | const operationObj = errorSpan.attributes.filter((obj) => obj.key === 'operation'); 101 | chai.assert.equal(typeObj[0].value.stringValue, helper.type.DOC, 'incorrect doc load type'); 102 | chai.assert.equal(operationObj[0].value.stringValue, helper.operations.LOAD, 'incorrect doc load operation'); 103 | done(); 104 | }, 5500); 105 | }).timeout(6000); 106 | 107 | it('should send error span as doc load if no parent span', (done) => { 108 | helper.createError(); 109 | setTimeout(() => { 110 | const spans = spyExporter.returnValues[0]; 111 | chai.assert.ok(spans.resourceSpans[0].instrumentationLibrarySpans[0], 'spans not created'); 112 | const span = spans.resourceSpans[0].instrumentationLibrarySpans[0]; 113 | const errorSpan = span.spans[0]; 114 | const errorEvent = errorSpan.events[0]; 115 | chai.assert.equal(span.spans.length, 1, 'more then one doc load span being created'); 116 | chai.assert.equal(errorEvent.attributes.length, 3, 'error event should have 3 attributes: error message, type and stacktrace') 117 | chai.assert.equal(errorSpan.name, '/', 'Span name was not converted to path name'); 118 | chai.assert.equal(errorSpan.events[0].name, 'exception', 'Error not added as event on doc load span'); 119 | chai.assert.deepEqual(errorSpan.events[0].attributes[0], { key: 'exception.message', value: { stringValue: 'my error' } }, 'exception didnt capture error message'); 120 | const typeObj = errorSpan.attributes.filter((obj) => obj.key === 'type'); 121 | const operationObj = errorSpan.attributes.filter((obj) => obj.key === 'operation'); 122 | chai.assert.equal(typeObj[0].value.stringValue, helper.type.DOC, 'incorrect doc load type'); 123 | chai.assert.equal(operationObj[0].value.stringValue, helper.operations.LOAD, 'incorrect doc load operation'); 124 | done(); 125 | }, 5500); 126 | }).timeout(6000); 127 | }); 128 | 129 | after(() => sandbox.restore()); 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | 5 | 6 |
7 |

8 | 9 | > **Note:** This repository is archived since Epsagon is no longer supported 10 | 11 | # Epsagon Tracing for Web 12 | 13 | This package provides tracing to front end of web applications for the collection of distributed tracing and performance metrics in [Epsagon](https://app.epsagon.com/?utm_source=github). 14 | 15 | ## Contents 16 | 17 | - [Installation](#installation) 18 | - [Usage](#usage) 19 | - [Custom Tags](#custom-tags) 20 | - [Configuration](#configuration) 21 | - [Trace Header Propagation](#trace-header-propagation) 22 | - [FAQ](#faq) 23 | - [Getting Help](#getting-help) 24 | - [Opening Issues](#opening-issues) 25 | - [License](#license) 26 | 27 | ## Installation 28 | 29 | To install Epsagon, simply run: 30 | ```sh 31 | npm install @epsagon/web --save 32 | ``` 33 | 34 | ## Usage 35 | 36 | To initialize the tracer, import the SDK and call the init function before the start of your project. 37 | 38 | ```javascript 39 | import epsagon from '@epsagon/web' 40 | 41 | epsagon.init({ 42 | token: 'epsagon-token', 43 | appName: 'app-name-stage', 44 | }) 45 | ``` 46 | 47 | ### Import and install as webpack bundle 48 | 49 | ```html 50 | 51 | 57 | ``` 58 | 59 | 60 | ## Custom Tags 61 | 62 | To add additional information to spans there are two methods available. Batch add user identity information with the ```epsagon.identity``` function, or use the ```epsagon.tag``` function to add your own custom information. 63 | 64 | Options for ```epsagon.identify``` include { userId, userName, userEmail, companyId, companyName }. 65 | 66 | ```js 67 | epsagon.identify({ 68 | userId: '7128f1a08a95e46c', 69 | userName: 'John Doe', 70 | userEmail: 'john@doe.com', 71 | companyId: 'fcffa7328813e4', 72 | companyName: 'Epsagon' 73 | }) 74 | 75 | ``` 76 | 77 | Custom tags can only be added one at a time by passing a key and value to the tag function. 78 | 79 | ```js 80 | epsagon.tag('PurchaseId', '2ef5b4bfdd') 81 | ``` 82 | 83 | ## Configuration 84 | 85 | Advanced options can be configured as a parameter to the init() method. 86 | 87 | |Parameter |Type |Default |Description | 88 | |-------------------|-------|-------------|-----------------------------------------------------------------------------------| 89 | |token |String |- |Epsagon account token | 90 | |appName |String |`Epsagon Application`|Application name that will be set for traces | 91 | |collectorURL |String |-|The address of the trace collector to send trace to | 92 | |metadataOnly |Boolean|`false` |Whether to send only the metadata (`true`) or also the payloads (`false`) | 93 | |propagateTraceHeaderUrls |Array|`*` |Which outgoing requests to add traceparent headers to. Defaults to all. | 94 | |urlPatternsToIgnore |Array|[] |Which outgoing requests to ignore (and not add traceparent to. Default to [] | 95 | |networkSamplingRatio|Float |1.0 |How many spans are exported, configured between 0.0 (send nothing) to 1.0 (send everything)| 96 | |maxQueueSize |Integer|2048 |Maximum queue size (bytes), afterwhich spans are dropped | 97 | |scheduledDelayMillis|Integer|5000|Delay interval in milliseconds between two consecutive exports | 98 | |exportTimeoutMillis|Integer|30000|How many milliseconds the export can run before it is cancelled| 99 | |maxBatchSize |Integer|1024 |Maximum batch size (bytes) of every export. Has to be small or equal to maxQueueSize| 100 | |isEpsagonDisabled |Boolean|`false` |A flag to completely disable Epsagon (can be used for tests or locally) | 101 | |epsagonDebug |Boolean|`false` |Enable debug prints for troubleshooting. Note: if this flag is true, this will override the logLevel| 102 | |logLevel |String|`INFO` |The default Log level. Could be one of: ```DEBUG```, ```INFO```, ```WARN```, ```ERROR```, ```ALL```.| 103 | 104 | ### Trace Header Propagation 105 | By default all outgoing requests will be added with a `traceparent` header which allows Epsagon to connect the front end trace to the backend traces. Some external services will not accept a traceparent header on request. If you need to limit the traceparent headers to requests to internal services, pass in an array of the hosts you do want to connect to in the propagateTraceHeaderUrls param in the config. 106 | 107 | ```javascript 108 | import epsagon from '@epsagon/web' 109 | 110 | epsagon.init({ 111 | token: 'epsagon-token', 112 | appName: 'app-name-stage', 113 | propagateTraceHeaderUrls: ['localhost', 'sub.example.com'], 114 | urlPatternsToIgnore: [".*example.*", ".*abc.*"] 115 | }) 116 | ``` 117 | 118 | ## FAQ 119 | 120 | **Question:** I'm getting CORS errors in my application. 121 | 122 | **Answer:** epsagon-browser adds `traceparent` HTTP header to all outgoing HTTP calls. You should make sure your backend accepts this header. If you are using 3rd party services, you can use `propagateTraceHeaderUrls` parameter to only add the header to your urls. 123 | 124 | ## Getting Help 125 | 126 | If you have any issue around using the library or the product, please don't hesitate to: 127 | 128 | * Use the [documentation](https://docs.epsagon.com). 129 | * Use the help widget inside the product. 130 | * Open an issue in GitHub. 131 | 132 | 133 | ## Opening Issues 134 | 135 | If you encounter a bug with the Epsagon library, we want to hear about it. 136 | 137 | When opening a new issue, please provide as much information about the environment: 138 | * Library version, Node.js runtime version, dependencies, etc. 139 | * Snippet of the usage. 140 | * A reproducible example can really help. 141 | 142 | The GitHub issues are intended for bug reports and feature requests. 143 | For help and questions about Epsagon, use the help widget inside the product. 144 | 145 | 146 | ## License 147 | 148 | Provided under the MIT license. See LICENSE for details. 149 | 150 | Copyright 2021, Epsagon 151 | -------------------------------------------------------------------------------- /packages/web/src/instrumentation/fetchInstrumentation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* eslint-disable no-console */ 3 | /* eslint-disable max-len */ 4 | import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'; 5 | import { diag } from "@opentelemetry/api"; 6 | 7 | const api = require('@opentelemetry/api'); 8 | const core = require('@opentelemetry/core'); 9 | const semanticConventions1 = require('@opentelemetry/semantic-conventions'); 10 | 11 | class EpsagonFetchInstrumentation extends FetchInstrumentation { 12 | constructor(config, parentSpan, options) { 13 | super(config); 14 | this.epsParentSpan = parentSpan; 15 | this.globalOptions = options; 16 | } 17 | 18 | // has to be overridden in order to grab response obj before the stream is read and no longer useable 19 | /* eslint-disable no-undef */ 20 | _patchConstructor() { 21 | return (original) => { 22 | const plugin = this; 23 | return function patchConstructor(input, init) { 24 | diag.debug('input: ', input, ' init: ', init); 25 | const url = input instanceof Request ? input.url : input; 26 | const options = input instanceof Request ? input : init || {}; 27 | diag.debug('url: ', url, 'options: ', options); 28 | if (options.eps) { 29 | // if epsagon request, ignore and dont send through eps param 30 | diag.debug("epsagon request. ignore and don't send through eps param"); 31 | return original.apply(this, [url, {}]); 32 | } 33 | const createdSpan = plugin._createSpan(url, options); 34 | if (!createdSpan) { 35 | diag.debug('span was not created. apply the original function'); 36 | return original.apply(this, [input, init]); 37 | } 38 | const spanData = plugin._prepareSpanData(url); 39 | function endSpanOnError(span, error) { 40 | plugin._applyAttributesAfterFetch(span, options, error); 41 | plugin._endSpan(span, spanData, { 42 | status: error.status || 0, 43 | statusText: error.message, 44 | trace: error.trace, 45 | url, 46 | }); 47 | } 48 | function endSpanOnSuccess(span, response) { 49 | plugin._applyAttributesAfterFetch(span, options, response); 50 | if (response.status >= 200 && response.status < 400) { 51 | plugin._endSpan(span, spanData, response); 52 | } else { 53 | plugin._endSpan(span, spanData, { 54 | status: response.status, 55 | statusText: response.statusText, 56 | url, 57 | }); 58 | } 59 | } 60 | function onSuccess(span, resolve, response) { 61 | if (!response) { 62 | return; 63 | } 64 | try { 65 | const resClone = response.clone(); 66 | const resClone2 = response.clone(); 67 | const { body } = resClone; 68 | if (body && !plugin.epsParentSpan.isTest) { 69 | const reader = body.getReader(); 70 | const read = () => { 71 | reader.read().then(async ({ done }) => { 72 | if (done) { 73 | if (plugin.globalOptions && !plugin.globalOptions.metadataOnly) { 74 | const resHeaders = []; 75 | Object.entries(resClone2.headers).forEach((entry) => { 76 | if (entry[0] === 'content-length') { 77 | span.setAttribute('http.response_content_length_eps', parseInt(entry[1], 10)); 78 | } 79 | resHeaders.push(entry); 80 | }); 81 | span.setAttribute('http.response.body', (await resClone2.text()).substring(0, 5000)); 82 | if (resHeaders.length > 0) { 83 | span.setAttribute('http.response.headers', JSON.stringify(resHeaders)); 84 | } 85 | } 86 | endSpanOnSuccess(span, response); 87 | } else { 88 | read(); 89 | } 90 | }, (error) => { 91 | endSpanOnError(span, error); 92 | }); 93 | }; 94 | read(); 95 | } else { 96 | // some older browsers don't have .body implemented 97 | endSpanOnSuccess(span, response); 98 | } 99 | } finally { 100 | resolve(response); 101 | } 102 | } 103 | function onError(span, reject, error) { 104 | try { 105 | endSpanOnError(span, error); 106 | } finally { 107 | reject(error); 108 | } 109 | } 110 | return new Promise((resolve, reject) => api.context.with(api.trace.setSpan(api.context.active(), createdSpan), () => { 111 | diag.debug('Before add headers: url: ', url, 'options: ', options); 112 | plugin._addHeaders(options, url); 113 | diag.debug('After add headers: url: ', url , 'options: ', options); 114 | plugin._tasksCount += 1; 115 | return original 116 | .apply(this, [input, init]) 117 | .catch((ex) => { 118 | diag.debug(ex); 119 | diag.debug(JSON.stringify(ex)); 120 | }) 121 | .then(onSuccess.bind(this, createdSpan, resolve), onError.bind(this, createdSpan, reject)); 122 | })); 123 | }; 124 | }; 125 | } 126 | 127 | // create span copied over so parent span can be added at creation, additional attributes also added here 128 | _createSpan(url, options = {}) { 129 | if (core.isUrlIgnored(url, this._getConfig().ignoreUrls)) { 130 | diag.debug('ignoring span as url matches ignored url'); 131 | return undefined; 132 | } 133 | const method = (options.method || 'GET').toUpperCase(); 134 | const spanName = `HTTP ${method}`; 135 | diag.debug('create span: url: ', url , 'options: ', options); 136 | 137 | let span; 138 | if (this.globalOptions.metadataOnly) { 139 | span = { 140 | kind: api.SpanKind.CLIENT, 141 | attributes: { 142 | component: this.moduleName, 143 | [semanticConventions1.SemanticAttributes.HTTP_METHOD]: method, 144 | [semanticConventions1.SemanticAttributes.HTTP_URL]: url, 145 | }, 146 | }; 147 | } else { 148 | span = { 149 | kind: api.SpanKind.CLIENT, 150 | attributes: { 151 | component: this.moduleName, 152 | [semanticConventions1.SemanticAttributes.HTTP_METHOD]: method, 153 | [semanticConventions1.SemanticAttributes.HTTP_URL]: url, 154 | 'http.request.headers': JSON.stringify(options.headers), 155 | 'http.request.body': options.body, 156 | }, 157 | }; 158 | } 159 | return this.tracer.startSpan(spanName, span, this.epsParentSpan.currentSpan ? api.trace.setSpan(api.context.active(), this.epsParentSpan.currentSpan) : undefined); 160 | } 161 | } 162 | 163 | export default EpsagonFetchInstrumentation; 164 | -------------------------------------------------------------------------------- /packages/web/src/web-tracer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | /* eslint-disable no-console */ 3 | 4 | import { BatchSpanProcessor } from '@opentelemetry/tracing'; 5 | import { WebTracerProvider } from '@opentelemetry/web'; 6 | import { ZoneContextManager } from '@opentelemetry/context-zone'; 7 | import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api'; 8 | import { setGlobalErrorHandler, loggingErrorHandler, TraceIdRatioBasedSampler } from '@opentelemetry/core'; 9 | import EpsagonFetchInstrumentation from './instrumentation/fetchInstrumentation'; 10 | import EpsagonXMLHttpRequestInstrumentation from './instrumentation/xmlHttpInstrumentation'; 11 | import EpsagonDocumentLoadInstrumentation from './instrumentation/documentLoadInstrumentation'; 12 | import EpsagonExporter from './exporter'; 13 | import EpsagonUtils from './utils'; 14 | import EpsagonRedirectInstrumentation from './instrumentation/redirectInstrumentation'; 15 | import { DEFAULT_CONFIGURATIONS } from './consts'; 16 | 17 | const { CompositePropagator, HttpTraceContextPropagator } = require('@opentelemetry/core'); 18 | const parser = require('ua-parser-js'); 19 | const { registerInstrumentations } = require('@opentelemetry/instrumentation'); 20 | 21 | let existingTracer; 22 | let epsSpan; 23 | 24 | class EpsagonSpan { 25 | constructor(tracer) { 26 | const span = tracer.startSpan('epsagon_init', { 27 | attributes: { 28 | operation: 'page_load', 29 | type: 'browser', 30 | }, 31 | }); 32 | span.setStatus({ code: 0 }); 33 | span.end(); 34 | this._currentSpan = span; 35 | this._time = Date.now(); 36 | this.identifyFields = null; 37 | this.tags = {}; 38 | } 39 | 40 | get currentSpan() { 41 | if (this._time !== null && this._time + DEFAULT_CONFIGURATIONS.pageLoadTimeout >= Date.now()) { 42 | return this._currentSpan; 43 | } 44 | this.currentSpan = null; 45 | this._time = null; 46 | return null; 47 | } 48 | 49 | set currentSpan(span) { 50 | if (span) { 51 | this._currentSpan = span; 52 | this._time = Date.now(); 53 | } 54 | } 55 | } 56 | 57 | function identify(options) { 58 | if (epsSpan) { 59 | epsSpan.identifyFields = { 60 | userId: options.userId, 61 | userName: options.userName, 62 | userEmail: options.userEmail, 63 | companyId: options.companyId, 64 | companyName: options.companyName, 65 | }; 66 | } 67 | } 68 | 69 | function tag(key, value) { 70 | if (epsSpan) { 71 | epsSpan.tags[key] = value; 72 | } 73 | } 74 | 75 | function handleLogLevel(_logLevel) { 76 | let logLevel; 77 | switch (_logLevel) { 78 | case 'ALL': 79 | logLevel = DiagLogLevel.ALL; 80 | break; 81 | case 'DEBUG': 82 | logLevel = DiagLogLevel.DEBUG; 83 | break; 84 | case 'INFO': 85 | logLevel = DiagLogLevel.INFO; 86 | break; 87 | case 'WARN': 88 | logLevel = DiagLogLevel.WARN; 89 | break; 90 | case 'ERROR': 91 | logLevel = DiagLogLevel.ERROR; 92 | break; 93 | // Default is Open Telemetry default which is DiagLogLevel.INFO 94 | default: 95 | return; 96 | } 97 | diag.setLogger(new DiagConsoleLogger(), logLevel); 98 | } 99 | 100 | function init(_configData) { 101 | const configData = _configData; 102 | 103 | if (configData.logLevel) { 104 | handleLogLevel(configData.logLevel); 105 | } 106 | 107 | // Epsagon debug overrides configData.logLevel 108 | if (configData.epsagonDebug) { 109 | diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG); 110 | } 111 | 112 | diag.info('configData: ', configData); 113 | 114 | let samplingRatio = DEFAULT_CONFIGURATIONS.networkSamplingRatio; 115 | 116 | if (configData.networkSamplingRatio || configData.networkSamplingRatio === 0) { 117 | samplingRatio = configData.networkSamplingRatio; 118 | } 119 | 120 | if (configData.isEpsagonDisabled) { 121 | console.log('Epsagon disabled, tracing is not running'); 122 | return undefined; 123 | } 124 | 125 | if (existingTracer && !configData.isTest) { 126 | diag.info('tracer already initialized, remove duplicate initialization call'); 127 | return undefined; 128 | } 129 | 130 | if (!configData.token) { 131 | console.log('Epsagon token must be passed into initialization'); 132 | return undefined; 133 | } 134 | 135 | if (!configData.collectorURL) { 136 | configData.collectorURL = DEFAULT_CONFIGURATIONS.collectorURL; 137 | } 138 | 139 | const appName = configData.appName || DEFAULT_CONFIGURATIONS.appName; 140 | 141 | const collectorOptions = { 142 | serviceName: appName, 143 | url: configData.collectorURL, 144 | hosts: configData.hosts, 145 | headers: { 146 | 'X-Epsagon-Token': `${configData.token}`, 147 | }, 148 | metadataOnly: configData.metadataOnly, 149 | }; 150 | 151 | const maxExportBatchSize = configData.maxBatchSize || DEFAULT_CONFIGURATIONS.maxBatchSize; 152 | const maxQueueSize = configData.maxQueueSize || DEFAULT_CONFIGURATIONS.maxQueueSize; 153 | if (maxExportBatchSize > maxQueueSize) { 154 | diag.error('maxExportBatchSize cannot be bigger than maxQueueSize, could not start Epsagon'); 155 | return undefined; 156 | } 157 | 158 | const batchProcessorConfig = { 159 | maxExportBatchSize, 160 | maxQueueSize, 161 | scheduledDelayMillis: configData.scheduledDelayMillis || DEFAULT_CONFIGURATIONS.scheduledDelayMillis, 162 | exportTimeoutMillis: configData.exportTimeoutMillis || DEFAULT_CONFIGURATIONS.exportTimeoutMillis, 163 | }; 164 | 165 | setGlobalErrorHandler(loggingErrorHandler()); 166 | 167 | const provider = new WebTracerProvider({ sampler: new TraceIdRatioBasedSampler(samplingRatio) }); 168 | 169 | /* eslint-disable no-undef */ 170 | const userAgent = parser(navigator.userAgent); 171 | 172 | const exporter = new EpsagonExporter(collectorOptions, userAgent); 173 | 174 | provider.addSpanProcessor(new BatchSpanProcessor(exporter, batchProcessorConfig)); 175 | 176 | provider.register({ 177 | contextManager: new ZoneContextManager(), 178 | propagator: new CompositePropagator({ 179 | propagators: [ 180 | new HttpTraceContextPropagator(), 181 | ], 182 | }), 183 | }); 184 | 185 | const tracer = provider.getTracer(appName); 186 | existingTracer = true; 187 | epsSpan = new EpsagonSpan(tracer); 188 | 189 | if (configData.isTest) { 190 | epsSpan.isTest = true; 191 | } 192 | 193 | let whiteListedURLsRegex; 194 | if (configData.propagateTraceHeaderUrls) { 195 | const urlsList = configData.propagateTraceHeaderUrls; 196 | whiteListedURLsRegex = urlsList.length > 1 ? new RegExp(urlsList.join('|')) : new RegExp(urlsList); 197 | } else { 198 | whiteListedURLsRegex = /.+/; 199 | } 200 | 201 | let blackListedURLs = []; 202 | if (configData.urlPatternsToIgnore) { 203 | blackListedURLs = configData.urlPatternsToIgnore; 204 | blackListedURLs.forEach((item, index, arr) => { 205 | // eslint-disable-next-line no-param-reassign 206 | arr[index] = RegExp(item); 207 | }); 208 | } 209 | 210 | const isErrorCollectionDisabled = _configData.errorDisabled ? _configData.errorDisabled : false; 211 | 212 | registerInstrumentations({ 213 | tracerProvider: provider, 214 | instrumentations: [ 215 | new EpsagonDocumentLoadInstrumentation(epsSpan, isErrorCollectionDisabled), 216 | new EpsagonFetchInstrumentation({ 217 | ignoreUrls: blackListedURLs, 218 | propagateTraceHeaderCorsUrls: whiteListedURLsRegex, 219 | }, epsSpan, { metadataOnly: configData.metadataOnly }), 220 | new EpsagonXMLHttpRequestInstrumentation({ 221 | ignoreUrls: blackListedURLs, 222 | propagateTraceHeaderCorsUrls: whiteListedURLsRegex, 223 | }, epsSpan, { metadataOnly: configData.metadataOnly }), 224 | new EpsagonRedirectInstrumentation(tracer, epsSpan, DEFAULT_CONFIGURATIONS.redirectTimeout), 225 | ] 226 | }); 227 | 228 | return { tracer, epsSpan }; 229 | } 230 | 231 | export { 232 | init, identify, tag, EpsagonUtils, 233 | }; 234 | -------------------------------------------------------------------------------- /packages/web/src/exporter.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable max-len */ 3 | import { CollectorTraceExporter } from '@opentelemetry/exporter-collector'; 4 | import { diag } from '@opentelemetry/api'; 5 | import { loggingErrorHandler } from '@opentelemetry/core'; 6 | import EpsagonFormatter from './formatter'; 7 | import EpsagonResourceManager from './resource-manager'; 8 | import EpsagonIPCalculator from './ip-calculator'; 9 | import EpsagonUtils from './utils'; 10 | import { ROOT_TYPE, SPAN_ATTRIBUTES_NAMES } from './consts'; 11 | 12 | class EpsagonExporter extends CollectorTraceExporter { 13 | constructor(config, ua) { 14 | super(config); 15 | this.config = config; 16 | this.userAgent = ua; 17 | this.formatter = new EpsagonFormatter(config); 18 | this.resourceManager = new EpsagonResourceManager(config); 19 | EpsagonIPCalculator.calculate((data) => { 20 | this.userAgent.browser.ip = data.ip; 21 | this.userAgent.browser.country = data.country; 22 | this.userAgent.browser.regionName = data.regionName; 23 | this.userAgent.browser.city = data.city; 24 | }); 25 | } 26 | 27 | convert(spans) { 28 | try { 29 | const errorSpans = spans.filter((s) => s.exceptionData); 30 | const convertedSpans = super.convert(spans); 31 | let spansList = EpsagonUtils.getFirstResourceSpan(convertedSpans).instrumentationLibrarySpans; 32 | const rootSpan = { 33 | rootType: ROOT_TYPE.EPS, 34 | eps: {}, 35 | doc: {}, 36 | redirect: {}, 37 | }; 38 | const errSpan = { 39 | messages: [], 40 | }; 41 | 42 | Object.keys(spansList).forEach((spanIndex) => { 43 | const spanSubList = spansList[spanIndex].spans; 44 | diag.debug('Handle spans for index ', spanIndex); 45 | 46 | /* eslint-disable no-restricted-syntax */ 47 | for (const spanSubIndex in spanSubList) { 48 | if (spanSubList[spanSubIndex]) { 49 | let span = spanSubList[spanSubIndex]; 50 | let spanAttributes = span.attributes; 51 | let attributesLength = spanAttributes.length; 52 | 53 | if (span.name === ROOT_TYPE.EPS) { 54 | diag.debug('Handle epsagon_init span', span); 55 | rootSpan.eps.position = spanIndex; 56 | rootSpan.eps.subPosition = spanSubIndex; 57 | rootSpan.eps.spanId = span.spanId; 58 | const formattedSpan = EpsagonFormatter.formatDocumentLoadSpan(); 59 | span.name = formattedSpan.name; 60 | spanAttributes[attributesLength] = formattedSpan.browser; 61 | attributesLength += 1; 62 | spanAttributes[attributesLength] = formattedSpan.operation; 63 | attributesLength += 1; 64 | } 65 | if (span.name === ROOT_TYPE.ERROR) { 66 | if (span.attributes && span.attributes.length) { 67 | rootSpan.doc.position = spanIndex; 68 | errSpan.messages.push(EpsagonUtils.getFirstAttribute(span).value.stringValue); 69 | } 70 | break; 71 | } 72 | 73 | const httpHost = spanAttributes.filter((attr) => attr.key === SPAN_ATTRIBUTES_NAMES.HOST_HEADER); 74 | const userInteraction = spanAttributes.filter((attr) => attr.value.stringValue === SPAN_ATTRIBUTES_NAMES.USER_INTERACTION); 75 | const documentLoad = spanAttributes.filter((attr) => attr.value.stringValue === SPAN_ATTRIBUTES_NAMES.DOCUMENT_LOAD); 76 | const reactUpdates = spanAttributes.filter((attr) => attr.key === SPAN_ATTRIBUTES_NAMES.REACT_COMPONENT_NAME); 77 | 78 | if (httpHost.length > 0) { 79 | diag.debug('httpHost:', httpHost); 80 | const formattedHttpRequestSpan = this.formatter.formatHttpRequestSpan(span, httpHost, spanAttributes, attributesLength); 81 | spanAttributes = formattedHttpRequestSpan.spanAttributes; 82 | attributesLength = formattedHttpRequestSpan.attributesLength; 83 | span = formattedHttpRequestSpan.span; 84 | } else if (userInteraction.length > 0) { 85 | diag.debug('userInteraction:', userInteraction); 86 | const formattedSpan = EpsagonFormatter.formatUserInteractionSpan(spanAttributes, attributesLength); 87 | spanAttributes[attributesLength] = formattedSpan.userInteraction; 88 | attributesLength += 1; 89 | spanAttributes[attributesLength] = formattedSpan.operation; 90 | attributesLength += 1; 91 | } else if (documentLoad.length > 0 || reactUpdates.length > 0) { 92 | diag.debug('document load:', documentLoad); 93 | diag.debug('react updates:', reactUpdates); 94 | rootSpan.doc.position = spanIndex; 95 | 96 | // replace root span with document load 97 | if (span.name === SPAN_ATTRIBUTES_NAMES.DOCUMENT_LOAD_SPAN_NAME) { 98 | rootSpan.rootType = ROOT_TYPE.DOC; 99 | rootSpan.eps.remove = true; 100 | rootSpan.doc.subPosition = spanSubIndex; 101 | rootSpan.doc.parent = span.parentSpanId; 102 | diag.debug('replace root span with document load:', rootSpan); 103 | } 104 | 105 | const formattedSpan = EpsagonFormatter.formatDocumentLoadSpan(); 106 | span.name = formattedSpan.name; 107 | spanAttributes[attributesLength] = formattedSpan.browser; 108 | attributesLength += 1; 109 | spanAttributes[attributesLength] = formattedSpan.operation; 110 | attributesLength += 1; 111 | } else if (span.name === SPAN_ATTRIBUTES_NAMES.ROUTE_CHANGE) { 112 | diag.debug('span name is route_change'); 113 | rootSpan.rootType = ROOT_TYPE.REDIR; 114 | 115 | const formattedSpan = EpsagonFormatter.formatRouteChangeSpan(this.userAgent); 116 | span.name = formattedSpan.name; 117 | spanAttributes[attributesLength] = formattedSpan.obj; 118 | attributesLength += 1; 119 | 120 | rootSpan.redirect.position = spanIndex; 121 | rootSpan.redirect.subPosition = spanSubIndex; 122 | } 123 | 124 | const finalAttrs = this.addFinalGenericSpanAttrs(spanAttributes, attributesLength, span); 125 | attributesLength = finalAttrs.attributesLength; 126 | span = finalAttrs.span; 127 | spanAttributes = finalAttrs.spanAttributes; 128 | } 129 | } 130 | }); 131 | 132 | if (errorSpans && errorSpans.length > 0) { 133 | spansList = EpsagonExporter.handleErrors(errorSpans, spansList, rootSpan); 134 | diag.debug('Error spans:', spansList); 135 | } 136 | 137 | if (rootSpan.eps.remove && rootSpan.doc.parent === rootSpan.eps.spanId) { 138 | diag.debug('Remove document load span'); 139 | const docLoad = spansList[rootSpan.doc.position].spans[0]; 140 | docLoad.parentSpanId = undefined; 141 | docLoad.spanId = rootSpan.eps.spanId; 142 | spansList.splice(rootSpan.eps.position, 1); 143 | } 144 | 145 | const convertedSpansWithRecourseAtts = this.resourceManager.addResourceAttrs(convertedSpans, this.userAgent); 146 | diag.debug('converted spans:', convertedSpansWithRecourseAtts); 147 | return convertedSpansWithRecourseAtts; 148 | } catch (err) { 149 | diag.warn('error converting and exporting', err); 150 | return null; 151 | } 152 | } 153 | 154 | static handleErrors(errorSpans, _spansList, rootSpan) { 155 | diag.debug('handle errors:', errorSpans); 156 | const spansList = _spansList; 157 | if (rootSpan.rootType === ROOT_TYPE.REDIR || rootSpan.rootType === ROOT_TYPE.DOC) { 158 | diag.debug('rootType:', ROOT_TYPE); 159 | const type = rootSpan.rootType === ROOT_TYPE.REDIR ? ROOT_TYPE.REDIR : ROOT_TYPE.ROOT_TYPE_DOC; 160 | const rootSubList = spansList[rootSpan[type].position].spans; 161 | const rootSubPos = rootSpan[type].subPosition; 162 | 163 | // errors get converted from their own spans to an event on the root span 164 | Array.from(errorSpans.values()).forEach((error) => { 165 | rootSubList[rootSubPos].events.unshift({ 166 | name: ROOT_TYPE.EXCEPTION, 167 | attributes: error.exceptionData.attributes, 168 | }); 169 | }); 170 | rootSubList[rootSpan[type].subPosition].status.code = 2; 171 | spansList[rootSpan.doc.position].spans = spansList[rootSpan.doc.position].spans.filter((span) => span.name !== ROOT_TYPE.ERROR); 172 | } else { 173 | diag.debug('remove duplicate events and add attrs.'); 174 | /// remove duplicate events and add attrs 175 | const finalSpans = []; 176 | spansList[rootSpan.doc.position].spans.forEach((span) => { 177 | if (span.name === ROOT_TYPE.ERROR) { 178 | const errorData = errorSpans.filter((s) => s.traceID === span.traceID); 179 | const errorDataSpan = errorData && errorData.length ? errorData[0] : errorData; 180 | /* eslint-disable no-undef */ 181 | // eslint-disable-next-line no-param-reassign 182 | span.name = `${window.location.pathname}${window.location.hash}`; 183 | span.events.unshift({ 184 | name: ROOT_TYPE.EXCEPTION, 185 | attributes: errorDataSpan.exceptionData.attributes, 186 | }); 187 | finalSpans.push(span); 188 | } 189 | }); 190 | spansList[rootSpan.doc.position].spans = finalSpans; 191 | } 192 | return spansList; 193 | } 194 | 195 | addFinalGenericSpanAttrs(_spanAttributes, _attributesLength, _span) { 196 | const spanAttributes = _spanAttributes; 197 | const span = _span; 198 | let attributesLength = _attributesLength; 199 | // replace any user agent keys with eps name convention 200 | const httpUA = spanAttributes.filter((attr) => attr.key === SPAN_ATTRIBUTES_NAMES.HOST_USER_AGENT); 201 | if (httpUA.length) { httpUA[0].key = SPAN_ATTRIBUTES_NAMES.HOST_REQUEST_USER_AGENT; } 202 | /* eslint-disable no-undef */ 203 | spanAttributes[attributesLength] = { key: SPAN_ATTRIBUTES_NAMES.BROWSER_HOST, value: { stringValue: window.location.hostname } }; 204 | attributesLength += 1; 205 | /* eslint-disable no-undef */ 206 | spanAttributes[attributesLength] = { key: SPAN_ATTRIBUTES_NAMES.BROWSER_PATH, value: { stringValue: window.location.pathname } }; 207 | span.attributes = spanAttributes.filter((attr) => { 208 | if (this.config.metadataOnly) { 209 | return attr.key !== span && attr.key !== SPAN_ATTRIBUTES_NAMES.RESPONSE_CONTENT_LENGTH; 210 | } 211 | return attr.key !== SPAN_ATTRIBUTES_NAMES.RESPONSE_CONTENT_LENGTH_EPS; 212 | }); 213 | diag.debug('FinalGenericSpanAttrs:', spanAttributes); 214 | return { attributesLength, span, spanAttributes }; 215 | } 216 | 217 | // eslint-disable-next-line no-unused-vars 218 | send(objects, onSuccess, onError) { 219 | super.send(objects, onSuccess, loggingErrorHandler()); 220 | } 221 | } 222 | 223 | export default EpsagonExporter; 224 | --------------------------------------------------------------------------------