├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierrc.json
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── package-lock.json
├── package.json
├── src
├── Agent.ts
├── AgentRegistrar.ts
├── Environment.ts
├── Fault.ts
├── Metadata.ts
├── Transmitter.ts
├── agentHelpers
│ ├── deduplicate.ts
│ ├── index.ts
│ └── truncate.ts
├── handlers
│ └── express.ts
├── index.ts
├── sdk.ts
├── telemetry
│ ├── ConsoleTelemetry.ts
│ ├── NetworkTelemetry.ts
│ ├── TelemetryBuffer.ts
│ └── index.ts
├── types
│ ├── TrackJSCapturePayload.ts
│ ├── TrackJSError.ts
│ ├── TrackJSOptions.ts
│ └── index.ts
├── utils
│ ├── cli.ts
│ ├── isType.ts
│ ├── nestedAssign.ts
│ ├── patch.ts
│ ├── serialize.ts
│ ├── truncateString.ts
│ ├── userAgent.ts
│ └── uuid.ts
├── version.ts
└── watchers
│ ├── ConsoleWatcher.ts
│ ├── ExceptionWatcher.ts
│ ├── NetworkWatcher.ts
│ ├── RejectionWatcher.ts
│ ├── Watcher.ts
│ └── index.ts
├── test
├── Agent.test.ts
├── Environment.test.ts
├── Metadata.test.ts
├── agentHelpers
│ ├── deduplicate.test.ts
│ └── truncate.test.ts
├── handlers
│ └── express.test.ts
├── index.test.ts
├── integration
│ ├── express
│ │ └── index.js
│ ├── network
│ │ └── index.js
│ ├── require
│ │ └── index.js
│ └── transmit
│ │ ├── index.js
│ │ ├── server.cert
│ │ └── server.key
├── sdk.test.ts
├── telemetry
│ ├── ConsoleTelemetry.test.ts
│ └── TelemetryBuffer.test.ts
├── utils
│ ├── cli.test.ts
│ ├── isType.test.ts
│ ├── nestedAssign.test.ts
│ ├── patch.test.ts
│ ├── serialize.test.ts
│ ├── truncateString.test.ts
│ └── uuid.test.ts
└── watchers
│ ├── ConsoleWatcher.test.ts
│ └── NetworkWatcher.test.ts
├── tools
├── release-canary.ps1
├── release-production.ps1
├── teamcity.js
└── version.js
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | //registry.npmjs.org/:_authToken=${NPM_TOKEN}
2 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 18.18.2
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "printWidth": 120
4 | }
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.3.0
4 |
5 | - Added installation options for `network.enabled` and `network.error` to control whether network telemetry and errors are captured.
6 | - Fixed wrapping of `http.get` and `https.get` to add telemetry events.
7 |
8 | ## 1.2.0
9 |
10 | - Added `TrackJS.console` function for parity with the browser agent.
11 |
12 | ## 1.1.0
13 |
14 | - Update NetworkWatcher to instrument the `request` method instead of the private `_http_agent` method, which breaks on some platforms.
15 |
16 | ## 1.0.2
17 |
18 | Update packages and verify new node version
19 |
20 | - Upgraded internal dev tools
21 | - Upgraded Typescript
22 | - Verified compatibility with node 18 LTS
23 |
24 | ## 1.0.1
25 |
26 | Fixes required for Node 14 and 15.
27 |
28 | - Fixed express errors in async code would not be attributed to the request.
29 | - Fixed rejected promises not recorded in Node 15 due to serialization of strings.
30 | - Fixed error correlation was always being reset.
31 |
32 | ## 1.0.0
33 |
34 | - Stable Release.
35 |
36 | ## 0.0.12
37 |
38 | - Fixed issue where we could not discover modules (dependencies) in restrictive cloud environments.
39 |
40 | ## 0.0.11
41 |
42 | - Added `Access-Control-Expose-Headers` header for correlation header sharing.
43 |
44 | ## 0.0.6
45 |
46 | Updates from initial beta and configuring UI.
47 |
48 | - Updated capture URLs
49 | - Sending `applicationPlatform`
50 | - Using "real" entry values
51 | - Default metadata for hostname, username, and cwd
52 | - Stamping captured errors with a `TrackJS` key so customer can cross-reference
53 | - Optionally send correlation header to client-side
54 |
55 | ## 0.0.5
56 |
57 | Initial creation of the NodeJS Agent. Key differences with the Browser Agent:
58 |
59 | - No helper functions, `attempt`, `watch`, or `watchAll`.
60 | - No Visitor or Navigation Telemetry.
61 | - Fewer options to configure environment integration. (Wait until we need them)
62 | - No ability to customize the serializer. I think this is unused.
63 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # Terms of Service
2 |
3 | The License and Terms of Service for the TrackJS Browser Error Monitoring Agent is located at https://trackjs.com/terms/. The text is copied here for reference, please visit the link for official terms.
4 |
5 | ---
6 |
7 | These Terms of Service governs the use of the TrackJS Service (The “Service”) operated by TrackJS, LLC (or “TrackJS”). This Agreement sets forth the legally binding terms and conditions for your use of trackjs.com (the “Site”). “You” or similar terms means you, the person accessing trackjs.com, the business or entity on whose behalf you access trackjs.com, as well as any person on whose behalf you are using trackjs.com or who may have rights through you.
8 |
9 | By creating an account, by logging into the Service, and/or by accessing or using the Site in any manner, including, but not limited to, visiting or browsing the Site or contributing content or other materials to the Site, you acknowledge that you have reviewed and accept the terms of service, agree to become bound by its terms, and certify that you are an authorized representative of the entity purchasing the service, and that you have the right and authority to enter into this agreement on the entity’s behalf.
10 |
11 | ## Account Terms
12 |
13 | - You must be at least 13 years or older to use TrackJS.
14 | - You may not have more than one free account at a time.
15 |
16 | ## Grant of License and Restrictions on Use
17 |
18 | Subject to the terms of this Agreement and proper payment to TrackJS, TrackJS grants you a non-exclusive, non-transferable, limited right to use the Service and Error Tracking Agents (The “Agents”) solely for your own internal business purposes for the term of this Agreement. You may embed the Agents within your own materially-larger software product or similar item (“Customer’s Product”) and you may distribute the Agents to third parties as a supporting and component part of Customer’s Product, but you may not distribute the Service or Agents as a stand-alone or primary element to any third party. In addition, you will not (and you will not allow any thirty party to): copy, distribute, rent, lease, transfer or sublicense all or any portion of the Service or Agents to any third party other than as part of Customer’s Product explicitly permitted above; modify or prepare derivative works of the Service or Agents; use the Service or Agents in any commercial context or for any commercial purpose or in any commercial product including reselling the Service or Agents other than as permitted above as part of Customer’s Product; use the Service or Agents in any manner that threatens the integrity, performance or availability of the Service; or reverse engineer, decompile, or disassemble the Service or Agents.
19 |
20 | ## Grant of License to TrackJS
21 |
22 | You grant TrackJS a worldwide right to use, store, and reproduce the data gathered by TrackJS of your website (the “Data”) as necessary for TrackJS to (i) create reports or statistics of the Data for you; (ii) provide Service to you; and (iii) create reports or statistics using the Data in the aggregate, provided that no such report identifies you by name or other distinguishing mark.
23 |
24 | ## Ownership
25 |
26 | You acknowledge that the Service and Agents are the exclusive property of TrackJS. TrackJS retains all rights, title and interest in and to copyrights, trade secrets, trademarks and other intellectual property rights in the Services and Agents and you shall not acquire any right, title, or interest in the Services and/or Agents, except the right to use it in accordance with this Agreement. Any rights to the Services and/or Agents granted are licensed and not sold.
27 |
28 | ## Term and Termination
29 |
30 | Either party may terminate this Agreement at any time with notice. Upon termination TrackJS will stop providing and you will stop accessing Services. Further, all rights and license to the Agents immediately terminate, you will immediately stop using the Agents in any way, and you will immediately delete and not retain any and all copies of the Agents in your possession or control. In the event of any termination you will not be entitled to any refunds or any other fees. This Agreement will automatically terminate if you do not comply with any terms or conditions of this Agreement, including paying for the Services. All terms of this Agreement which by this nature are intended to survive termination of this Agreement shall survive.
31 |
32 | ## Warranty Disclaimers
33 |
34 | TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, EXCEPT AS EXPRESSLY PROVIDED FOR IN THIS AGREEMENT, THE AGENTS AND SERVICES ARE BEING PROVIDED “AS IS” AND “AS AVAILABLE” WITHOUT WARRANTY OF ANY KIND. TRACKJS DOES NOT WARRANT THAT THE SERVICE WILL MEET CUSTOMER’S REQUIREMENTS. TRACKJS HEREBY DISCLAIMS ALL WARRANTIES, EXPRESS, IMPLIED, OR STATUTORY, INCLUDING, WITHOUT LIMITATION, ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, AND ANY WARRANTIES AS TO NON-INFRINGEMENT.
35 |
36 | ## Limitation of Liability
37 |
38 | TRACKJS’ ENTIRE LIABILITY UNDER, FOR BREACH OF, OR ARISING OUT OF THIS AGREEMENT AND/OR RELATED TO THIS AGREEMENT, THE AGENTS AND SERVICES, IS LIMITED TO THE PAYMENTS ACTUALLY MADE BY THE CUSTOMER FOR THE SERVICE DURING THE TWELVE (12) MONTHS PRIOR TO THE DATE OF THE EVENT GIVING RISE TO ANY LIABILITY. UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, TORT, CONTRACT, OR OTHERWISE, SHALL TRACKJS BE LIABLE TO YOU OR ANY OTHER PERSON FOR ANY INDIRECT, SPECIAL INCIDENTAL, EXEMPLARY, PUNITIVE OR CONSEQUENTIAL DAMAGES OF ANY KIND, INCLUDING WITHOUT LIMITATION, LOST PROFITS, LOSSES OR EXPENSES, WHETHER OR NOT TRACKJS WAS ADVISED OF, KNEW OF SHOULD HAVE KNOWN OF THE POSSIBILITY OF SUCH LOSS OR DAMAGE.
39 |
40 | ## Indemnification
41 |
42 | You agree to indemnify and hold TrackJS, and its subsidiaries, affiliates, officers, directors, agents, partners and employees, harmless from any claim or demand, including reasonable attorneys’ fees, made by any third party due to or arising out of the Services provided under this Agreement or any act or omission by you (including the entity on whose behalf you are entering into this Agreement). This indemnity obligation will survive the expiration or termination of this Agreement by either party for any reason.
43 |
44 | ## Confidentiality
45 |
46 | TrackJS acknowledges that you may disclose non-public, confidential information to TrackJS under this Agreement and you acknowledge that the Agents and Services provided to you, and the terms and conditions of this Agreement are confidential and proprietary to TrackJS. Each party agrees to take all reasonably necessary action, including appropriate instructions and agreements with employees and agents, to protect such confidential and proprietary information of the other party from unauthorized disclosure. In the event of any breach of this section, each party acknowledges that the non-breaching party would suffer irreparable harm and shall therefore be entitled to seek injunctive relief without the necessity of posting bond. You also acknowledge that infringement or unauthorized copying of the intellectual property of TrackJS would cause irreparable harm to TrackJS.
47 |
48 | ## Publicity
49 |
50 | You are permitted to state publicly that you are a Subscriber of TrackJS. You agree that TrackJS may include your name and trademarks in a list of TrackJS Customers, online or in promotional materials. You also agree that TrackJS may verbally reference you as a customer of the Service. You may opt out of the provisions in this Section by e-mailing a request to hello@trackjs.com.
51 |
52 | ## Governing Law and Venue
53 |
54 | The Site is controlled and operated within the United States, and is not intended to subject TrackJS to any the law or jurisdiction outside of the United States. The Site does not constitute any contract with any jurisdiction outside the State of Minnesota. Use of this Site is prohibited in any jurisdiction having laws that would void this Agreement in whole or essential part or which makes accessing the Site illegal. This Agreement is entered into and performed in the State of Minnesota, United States of America. It is governed by and shall be construed under the laws of Minnesota, exclusive of any choice of law or conflict of laws provisions. In any claim or action directly or indirectly arising under this Agreement or related to the Site, Services or Agents, each party irrevocably submits to the personal jurisdiction of the Minnesota State District Court sitting in Washington County, Minnesota or of the United States Court for the District of Minnesota. Each party waives any jurisdictional, venue or inconvenient forum objections to these courts. You agree that you shall pursue any claim against TrackJS in your individual capacity only, and you will not participate in any collective or so-called “class” action against us.
55 |
56 | ## General Provisions
57 |
58 | This Agreement is the complete and exclusive statement of the agreement between us concerning its subject matter and supersedes all prior agreements and representations between the parties. Any waiver of or modification to the terms of this Agreement will not be effective unless executed in writing and signed by TrackJS. If any provision of this Agreement is held to be unenforceable, in whole or in part, such holding shall not affect the validity of the other provisions of this Agreement. The software is controlled by U.S. Export Regulations, and it may not be exported to or used by embargoed countries or individuals. You may not assign this Agreement or any license granted under this Agreement without our prior written consent, which may be granted or not granted in our sole judgment. By agreeing to this Agreement you also agree to our collection, use and disclosure of information as described in the most-recent version of our Privacy Policy [https://trackjs.com/privacy] which is incorporated here by reference.
59 |
60 | ## Changes To This Agreement
61 |
62 | TrackJS reserves the right, at our sole discretion, to modify or replace these Terms of Service by posting the updated terms on the Site. Your continued use of the Site after any such changes constitutes your acceptance of the new Terms and Conditions.
63 |
64 | Please review this Agreement periodically for changes. If you do not agree to any of this Agreement or any changes to this Agreement, do not use, access or continue to access the Site or discontinue any use of the Site immediately.
65 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | # TrackJS Agent for NodeJS
9 |
10 | ## Reference
11 |
12 |
13 | - [TrackJS NodeJS Documentation](https://docs.trackjs.com/node-agent/installation/)
14 |
15 | ## Usage
16 |
17 | To use the Agent, call `TrackJS.install(options)` as soon as possible in your code. It will install the monitors into the environment.
18 |
19 | ```javascript
20 | // ES5
21 | const TrackJS = require("trackjs-node").TrackJS;
22 | // ES6
23 | import { TrackJS } from "trackjs-node";
24 |
25 | TrackJS.install({
26 | token: "YOUR_TOKEN"
27 | /* other options */
28 | });
29 | ```
30 |
31 | To add more context to your errors, add context and metadata through the agent.
32 |
33 | ```javascript
34 | TrackJS.configure({
35 | sessionId: "session",
36 | version: "1.0.0",
37 | userId: "frank@gmail.com"
38 | });
39 |
40 | // or add arbitrary keys for whatever you think is important
41 | TrackJS.addMetadata({
42 | foo: "bar"
43 | });
44 | ```
45 |
46 | TrackJS will automatically gather Telemetry and send errors. If you want to trigger these events yourself, you can.
47 |
48 | ```javascript
49 | TrackJS.addLogTelemetry("warn", [
50 | "a warning message",
51 | {
52 | /*state object*/
53 | }
54 | ]);
55 |
56 | TrackJS.track(new Error("everything has gone wrong!"));
57 | ```
58 |
59 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "trackjs-node",
3 | "version": "1.3.0",
4 | "description": "TrackJS Error Tracking agent for NodeJS",
5 | "keywords": [
6 | "error-tracking",
7 | "error-monitoring",
8 | "error-reporting",
9 | "error-handling",
10 | "logging",
11 | "javascript",
12 | "nodejs",
13 | "debugging",
14 | "debugger",
15 | "debugging-tool",
16 | "error",
17 | "trackjs",
18 | "trackjs-node",
19 | "trackjs-agent"
20 | ],
21 | "main": "dist/index.js",
22 | "types": "dist/index.d.ts",
23 | "scripts": {
24 | "build": "tsc -p tsconfig.json",
25 | "clean": "rimraf dist",
26 | "fix": "prettier --write \"{src,test}/**/*.ts\"",
27 | "lint": "prettier --check \"{src,test}/**/*.ts\"",
28 | "start": "run-s test:watch",
29 | "teamcity": "node ./tools/teamcity",
30 | "test": "run-s test:jest test:express test:network test:require test:transmit",
31 | "test:express": "node ./test/integration/express",
32 | "test:network": "node ./test/integration/network",
33 | "test:require": "node ./test/integration/require",
34 | "test:transmit": "node ./test/integration/transmit",
35 | "test:jest": "jest --forceExit",
36 | "test:watch": "jest --watch",
37 | "version": "node ./tools/version"
38 | },
39 | "repository": {
40 | "type": "git",
41 | "url": "git+https://github.com/TrackJs/TrackJS-Node.git"
42 | },
43 | "author": {
44 | "name": "TrackJS",
45 | "email": "hello@trackjs.com",
46 | "url": "https://trackjs.com/"
47 | },
48 | "homepage": "https://trackjs.com/",
49 | "license": "SEE LICENSE IN LICENSE.md",
50 | "bugs": {
51 | "email": "hello@trackjs.com",
52 | "url": "https://github.com/TrackJs/TrackJS-Node/issues"
53 | },
54 | "files": [
55 | "dist/**/*"
56 | ],
57 | "devDependencies": {
58 | "@types/jest": "29.5.6",
59 | "@types/node": "18.18.2",
60 | "axios": "1.5.1",
61 | "express": "4.18.2",
62 | "jest": "29.7.0",
63 | "jest-teamcity-reporter": "0.9.0",
64 | "npm-run-all": "4.1.5",
65 | "prettier": "1.18.2",
66 | "request": "2.88.0",
67 | "rimraf": "2.6.3",
68 | "simple-git": "1.116.0",
69 | "ts-jest": "29.1.1",
70 | "typescript": "4.8.4"
71 | },
72 | "jest": {
73 | "testResultsProcessor": "jest-teamcity-reporter",
74 | "roots": [
75 | "/test",
76 | "/src"
77 | ],
78 | "transform": {
79 | "^.+\\.ts$": "ts-jest"
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/src/Agent.ts:
--------------------------------------------------------------------------------
1 | import os from "os";
2 | import { TrackJSCapturePayload, TrackJSInstallOptions, TrackJSOptions, TrackJSConsole, TrackJSNetwork } from "./types";
3 | import { isFunction } from "./utils/isType";
4 | import { TelemetryBuffer, ConsoleTelemetry } from "./telemetry";
5 | import { Metadata } from "./Metadata";
6 | import { Environment } from "./Environment";
7 | import { transmit } from "./Transmitter";
8 | import { deduplicate, truncate } from "./agentHelpers";
9 | import { uuid } from "./utils/uuid";
10 | import { RELEASE_VERSION } from "./version";
11 | import { TrackJSEntry } from "./types/TrackJSCapturePayload";
12 | import { nestedAssign } from "./utils/nestedAssign";
13 |
14 | export class Agent {
15 | static defaults: TrackJSOptions = {
16 | token: "",
17 | application: "",
18 | defaultMetadata: true,
19 | dependencies: true,
20 | errorURL: "https://capture.trackjs.com/capture/node",
21 | faultURL: "https://usage.trackjs.com/fault.gif",
22 | network: {
23 | error: true,
24 | enabled: true
25 | },
26 | sessionId: "",
27 | usageURL: "https://usage.trackjs.com/usage.gif",
28 | userId: "",
29 | version: ""
30 | };
31 |
32 | environment = new Environment();
33 | metadata = new Metadata();
34 | options: TrackJSOptions;
35 | telemetry = new TelemetryBuffer(30);
36 |
37 | private _onErrorFns = [];
38 |
39 | constructor(options: TrackJSInstallOptions) {
40 | this.options = nestedAssign({}, Agent.defaults, options);
41 |
42 | if (isFunction(options.onError)) {
43 | this.onError(options.onError);
44 | delete this.options.onError;
45 | }
46 |
47 | this.metadata = new Metadata(this.options.metadata);
48 | delete this.options.metadata;
49 |
50 | if (this.options.defaultMetadata) {
51 | this.metadata.add("hostname", os.hostname());
52 | this.metadata.add("username", os.userInfo().username);
53 | this.metadata.add("cwd", process.cwd());
54 | if (process.mainModule) {
55 | this.metadata.add("filename", process.mainModule.filename);
56 | }
57 | }
58 | }
59 |
60 | /**
61 | * Capture an error report.
62 | *
63 | * @param error {Error} Error to be captured to the TrackJS Service.
64 | * @param entry {TrackJSEntry} Source type of the error.
65 | * @returns {Boolean} `false` if the error was ignored.
66 | */
67 | captureError(error: Error, entry: TrackJSEntry): boolean {
68 | // bail out if we've already captured this error instance on another path.
69 | if (error["__trackjs__"]) {
70 | return false;
71 | }
72 | if (!((error as any) instanceof Error)) {
73 | error = new Error("" + error);
74 | }
75 | Object.defineProperty(error, "__trackjs__", {
76 | value: true,
77 | enumerable: false
78 | });
79 |
80 | let report = this.createErrorReport(error, entry);
81 | let hasIgnored = false;
82 |
83 | [deduplicate, truncate, ...this._onErrorFns].forEach((fn) => {
84 | if (!hasIgnored) {
85 | try {
86 | hasIgnored = !fn(report);
87 | } catch (e) {
88 | // Error in user-provided callback. We want to proceed, but notify them
89 | // that their code has failed.
90 | report.console.push({
91 | severity: "error",
92 | message: "Your TrackJS onError handler failed: " + e.message,
93 | timestamp: new Date().toISOString()
94 | });
95 | }
96 | }
97 | });
98 | if (hasIgnored) {
99 | return false;
100 | }
101 |
102 | // Adding a record of this error to telemetry so that future errors have
103 | // an easy reference.
104 | // TODO replace with a better telemetry type
105 | this.telemetry.add("c", new ConsoleTelemetry("error", [error]));
106 |
107 | transmit({
108 | url: this.options.errorURL,
109 | method: "POST",
110 | queryParams: {
111 | token: this.options.token,
112 | v: RELEASE_VERSION
113 | },
114 | payload: report
115 | });
116 |
117 | // Tag the error with our information so that if the user is logging elsewhere,
118 | // they can cross-reference with our data.
119 | error["TrackJS"] = {
120 | correlationId: report.customer.correlationId,
121 | entry: report.entry,
122 | url: "https://my.trackjs.com/details/correlationid/" + report.customer.correlationId
123 | };
124 |
125 | return true;
126 | }
127 |
128 | /**
129 | * Capture a usage record.
130 | */
131 | captureUsage(): void {
132 | transmit({
133 | url: this.options.usageURL,
134 | method: "GET",
135 | queryParams: {
136 | token: this.options.token,
137 | correlationId: this.options.correlationId,
138 | application: this.options.application
139 | }
140 | });
141 | }
142 |
143 | /**
144 | * Creates a copy of the current agent and the contextual logs and event
145 | * handlers. This allows for cloned objects to be later modified independently
146 | * of the parent.
147 | *
148 | * @param options {TrackJSOptions} Override the installation settings.
149 | */
150 | clone(options?: TrackJSOptions): Agent {
151 | let cloned = new Agent(Object.assign({}, this.options, options) as TrackJSInstallOptions);
152 | cloned.metadata = this.metadata.clone();
153 | cloned.telemetry = this.telemetry.clone();
154 | cloned.environment = this.environment.clone();
155 | this._onErrorFns.forEach((fn) => cloned.onError(fn));
156 |
157 | if (options && options.metadata) {
158 | cloned.metadata.add(options.metadata);
159 | }
160 |
161 | return cloned;
162 | }
163 |
164 | /**
165 | * Update the agent configuration options.
166 | *
167 | * @param options Option values to be updated.
168 | */
169 | configure(options: TrackJSOptions) {
170 | this.options = Object.assign(this.options, options);
171 | }
172 |
173 | /**
174 | * Generate a full error report payload for a given error with the context logs
175 | * gathered by this agent.
176 | *
177 | * @param error {Error} Error to base for the report.
178 | */
179 | createErrorReport(error: Error, entry: TrackJSEntry): TrackJSCapturePayload {
180 | let now = new Date();
181 | return {
182 | agentPlatform: "node",
183 | bindStack: null,
184 | bindTime: null,
185 | console: this.telemetry.getAllByCategory("c") as Array,
186 | customer: {
187 | application: this.options.application,
188 | correlationId: this.options.correlationId,
189 | sessionId: this.options.sessionId,
190 | token: this.options.token,
191 | userId: this.options.userId,
192 | version: this.options.version
193 | },
194 | entry: entry,
195 | environment: {
196 | age: now.getTime() - this.environment.start.getTime(),
197 | dependencies: this.options.dependencies ? this.environment.getDependencies() : {},
198 | originalUrl: this.environment.url,
199 | referrer: this.environment.referrerUrl,
200 | userAgent: this.environment.userAgent
201 | },
202 | file: "",
203 | message: error.message,
204 | metadata: this.metadata.get(),
205 | nav: [],
206 | network: this.telemetry.getAllByCategory("n") as Array,
207 | url: this.environment.url,
208 | stack: error.stack,
209 | throttled: 0,
210 | timestamp: now.toISOString(),
211 | visitor: [],
212 | version: RELEASE_VERSION
213 | };
214 | }
215 |
216 | /**
217 | * Attach a event handler to Errors. Event handlers will be called in order
218 | * they were attached.
219 | *
220 | * @param func {Function} Event handler that accepts a `TrackJSCapturePayload`.
221 | * Returning `false` from the handler will cause the Error to be ignored.
222 | */
223 | onError(func: (payload: TrackJSCapturePayload) => boolean): void {
224 | this._onErrorFns.push(func);
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/src/AgentRegistrar.ts:
--------------------------------------------------------------------------------
1 | import domain, { Domain } from "domain";
2 | import { Agent } from "./Agent";
3 |
4 | class _AgentRegistrar {
5 | private _primaryAgent: Agent;
6 | private _ref = Symbol("TrackJS Agent Registrar");
7 |
8 | init(primaryAgent: Agent): void {
9 | this._primaryAgent = primaryAgent;
10 | }
11 |
12 | /**
13 | * Finds or creates an agent for the current state. Looks for the current running
14 | * domain.
15 | *
16 | * @param activeDomain {Domain} Active Domain override.
17 | * @see https://nodejs.org/api/domain.html
18 | */
19 | getCurrentAgent(activeDomain?: Domain): Agent {
20 | activeDomain = activeDomain || domain["active"];
21 | if (!activeDomain) {
22 | return this._primaryAgent;
23 | }
24 |
25 | if (!activeDomain[this._ref]) {
26 | let domainAgent = this._primaryAgent.clone();
27 | activeDomain[this._ref] = domainAgent;
28 | }
29 |
30 | return activeDomain[this._ref];
31 | }
32 |
33 | close(): void {
34 | this._primaryAgent = null;
35 | /**
36 | * NOTE [Todd Gardner] We don't cleanup the "child" agents that have been
37 | * attached to domains, because we cannot hold a reference to them without
38 | * preventing GC. They will be disposed along with the domains they are
39 | * attached to.
40 | */
41 | }
42 | }
43 |
44 | /**
45 | * Singleton
46 | */
47 | export const AgentRegistrar = new _AgentRegistrar();
48 |
--------------------------------------------------------------------------------
/src/Environment.ts:
--------------------------------------------------------------------------------
1 | import { userAgent } from "./utils/userAgent";
2 | import { dirname, join } from "path";
3 | import { existsSync, readFileSync } from "fs";
4 |
5 | /**
6 | * Attributes about the current operating environment.
7 | */
8 | export class Environment {
9 | referrerUrl: string = "";
10 | start: Date = new Date();
11 | url: string = "";
12 | userAgent: string = userAgent;
13 | static dependencyCache: { [name: string]: string } = null;
14 |
15 | /**
16 | * Returns a copy of the Environment.
17 | */
18 | clone(): Environment {
19 | return Object.assign(new Environment(), this);
20 | }
21 |
22 | /**
23 | * Get the current environmental dependencies.
24 | */
25 | getDependencies(): { [name: string]: string } {
26 | if (!Environment.dependencyCache) {
27 | this.discoverDependencies();
28 | }
29 | return Object.assign({}, Environment.dependencyCache);
30 | }
31 |
32 | /**
33 | * Discover the environment modules and versions. This is expensive, so we
34 | * should only do it once.
35 | *
36 | * @param _bustCache {Boolean} Whether the existing dependency cache should be
37 | * discarded and rediscovered.
38 | */
39 | discoverDependencies(_bustCache?: boolean): void {
40 | if (Environment.dependencyCache && !_bustCache) {
41 | return;
42 | }
43 |
44 | Environment.dependencyCache = {};
45 |
46 | let pathCache = {};
47 | let rootPaths = (require.main && require.main.paths) || [];
48 | let moduleFiles = require.cache ? Object.keys(require.cache as {}) : [];
49 |
50 | function recurseUpDirTree(dir: string, roots: string[] = [], maxDepth = 10) {
51 | if (maxDepth === 0 || !dir || pathCache[dir] || roots.indexOf(dir) >= 0) {
52 | return;
53 | }
54 |
55 | pathCache[dir] = 1;
56 | let packageFile = join(dir, "package.json");
57 |
58 | if (existsSync(packageFile)) {
59 | try {
60 | let packageInfo = JSON.parse(readFileSync(packageFile, "utf8"));
61 | Environment.dependencyCache[packageInfo.name] = packageInfo.version;
62 | } catch (err) {
63 | /* bad json */
64 | }
65 | } else {
66 | // Recursion!
67 | return recurseUpDirTree(dirname(dir), roots, maxDepth--);
68 | }
69 | }
70 |
71 | moduleFiles.forEach((moduleFile) => {
72 | let modulePath = dirname(moduleFile);
73 | recurseUpDirTree(modulePath, rootPaths);
74 | });
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Fault.ts:
--------------------------------------------------------------------------------
1 | import { transmit } from "./Transmitter";
2 | import { AgentRegistrar } from "./AgentRegistrar";
3 | import { serialize } from "./utils/serialize";
4 | import { isError } from "./utils/isType";
5 | import { Agent } from "./Agent";
6 | import { RELEASE_VERSION, RELEASE_NAME, RELEASE_HASH } from "./version";
7 |
8 | export function captureFault(fault: any) {
9 | let error = isError(fault) ? fault : new Error(serialize(fault));
10 | let agent = AgentRegistrar.getCurrentAgent();
11 | let agentOptions = agent ? agent.options : Agent.defaults;
12 |
13 | transmit({
14 | url: agentOptions.faultURL,
15 | method: "GET",
16 | queryParams: {
17 | token: agentOptions.token,
18 | file: "",
19 | msg: error.message,
20 | stack: (error.stack || "").substr(0, 1000),
21 | url: "",
22 | a: RELEASE_NAME,
23 | v: RELEASE_VERSION,
24 | h: RELEASE_HASH
25 | }
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/src/Metadata.ts:
--------------------------------------------------------------------------------
1 | import { serialize } from "./utils/serialize";
2 | import { isString } from "./utils/isType";
3 |
4 | interface dictionary {
5 | [key: string]: string;
6 | }
7 |
8 | /**
9 | * User-defined Metadata about the current environment.
10 | */
11 | export class Metadata {
12 | private _hash: dictionary = {};
13 |
14 | constructor(initialMeta?: dictionary) {
15 | if (initialMeta) {
16 | this.add(initialMeta);
17 | }
18 | }
19 |
20 | /**
21 | * Add a key-value pair of strings to metadata.
22 | * Or add a Dictionary Object of key-value pairs to metadata.
23 | *
24 | * @param meta {String|Dictionary} key to add, or Dictionary Object.
25 | * @param value {String} value to add.
26 | * @example
27 | * metadata.add('foo', 'bar')
28 | * metadata.add({ 'foo': 'bar', 'bar': 'baz' })
29 | */
30 | add(meta: string | dictionary, value?: string): void {
31 | if (isString(meta)) {
32 | this._hash[meta as string] = value;
33 | } else {
34 | Object.keys(meta).forEach((key) => {
35 | this._hash[serialize(key)] = serialize(meta[key]);
36 | });
37 | }
38 | }
39 |
40 | /**
41 | * Creates a copy of the metadata.
42 | */
43 | clone(): Metadata {
44 | let cloned = new Metadata();
45 | cloned.add(
46 | this.get().reduce((pre, cur) => {
47 | pre[cur.key] = cur.value;
48 | return pre;
49 | }, {})
50 | );
51 | return cloned;
52 | }
53 |
54 | /**
55 | * Returns the contents of metadata as an Array of Objects.
56 | */
57 | get(): Array<{ key: string; value: string }> {
58 | return Object.keys(this._hash).map((key) => {
59 | return { key, value: this._hash[key] };
60 | });
61 | }
62 |
63 | /**
64 | * Remove a key from metadata.
65 | * Or remove a Dictionary Object of keys from metadata. The values in the
66 | * dictionary do not matter.
67 | *
68 | * @param meta {String|Dictionary} key to add, or Dictionary Object.
69 | * @example
70 | * metadata.remove('foo')
71 | * metadata.remove({ 'foo': '', 'bar': '' })
72 | */
73 | remove(meta: string | dictionary): void {
74 | if (isString(meta)) {
75 | delete this._hash[meta as string];
76 | } else {
77 | Object.keys(meta).forEach((key) => {
78 | delete this._hash[serialize(key) as string];
79 | });
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Transmitter.ts:
--------------------------------------------------------------------------------
1 | import https from "https";
2 | import { URL, URLSearchParams } from "url";
3 | import { TrackJSError } from "./types";
4 | import { userAgent } from "./utils/userAgent";
5 | import { captureFault } from "./Fault";
6 |
7 | export type TransmitOptions = {
8 | /**
9 | * Base URL to transmit.
10 | */
11 | url: string;
12 |
13 | /**
14 | * HTTP Method to send.
15 | * GET, POST
16 | */
17 | method: string;
18 |
19 | /**
20 | * Data to be included as querystring parameters.
21 | */
22 | queryParams?: { [name: string]: string };
23 |
24 | /**
25 | * Data payload to be transmitted.
26 | */
27 | payload?: Object;
28 | };
29 |
30 | /**
31 | * Transmit a message to the TrackJS Service
32 | *
33 | * @param options {TransmitOptions}
34 | */
35 | export function transmit(options: TransmitOptions) {
36 | let url = new URL(options.url);
37 |
38 | if (url.protocol !== "https:") {
39 | throw new TrackJSError(`unsupported url ${options.url}`);
40 | }
41 |
42 | if (options.queryParams) {
43 | url.search = new URLSearchParams(options.queryParams).toString();
44 | }
45 |
46 | let req = https.request(
47 | {
48 | method: options.method,
49 | hostname: url.hostname,
50 | port: url.port,
51 | path: `${url.pathname}${url.search}`,
52 | __trackjs__: true // prevent our requests from being observed by `NetworkWatcher`
53 | } as https.RequestOptions,
54 | (resp) => null
55 | );
56 |
57 | if (options.method === "POST" && options.payload) {
58 | let body = JSON.stringify(options.payload);
59 | req.setHeader("User-Agent", userAgent);
60 | req.write(body);
61 | }
62 |
63 | if (options.url.indexOf("fault.gif") < 0) {
64 | req.on("error", captureFault);
65 | }
66 |
67 | req.end();
68 | }
69 |
--------------------------------------------------------------------------------
/src/agentHelpers/deduplicate.ts:
--------------------------------------------------------------------------------
1 | import { TrackJSCapturePayload } from "../types";
2 |
3 | const _history = new Set();
4 |
5 | /**
6 | * Deduplicate errors from being sent.
7 | *
8 | * @param payload
9 | */
10 | export function deduplicate(payload: TrackJSCapturePayload): boolean {
11 | let fingerprint = payload.customer.token + payload.message + payload.stack;
12 |
13 | if (!_history.has(fingerprint)) {
14 | _history.add(fingerprint);
15 | setTimeout(() => {
16 | _history.delete(fingerprint);
17 | }, 1000);
18 | return true;
19 | }
20 |
21 | return false;
22 | }
23 |
--------------------------------------------------------------------------------
/src/agentHelpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./deduplicate";
2 | export * from "./truncate";
3 |
--------------------------------------------------------------------------------
/src/agentHelpers/truncate.ts:
--------------------------------------------------------------------------------
1 | import { TrackJSCapturePayload } from "../types";
2 | import { truncateString } from "../utils/truncateString";
3 |
4 | const MAX_SIZE = 100000;
5 |
6 | function getByteLength(str: string): number {
7 | return Buffer.byteLength(str, "utf8");
8 | }
9 |
10 | function isConsoleTelemetryTooBig(payload: TrackJSCapturePayload): boolean {
11 | return (
12 | payload.console.reduce((prev, curr) => {
13 | return prev + (curr.message || "").length;
14 | }, 0) >= 80000
15 | );
16 | }
17 |
18 | /**
19 | * Truncate the contents of the payload to help ensure that it will be accepted
20 | * by the TrackJS Server. Truncates the contents of Console Telemetry until
21 | *
22 | * @param payload
23 | */
24 | export function truncate(payload: TrackJSCapturePayload): boolean {
25 | if (getByteLength(JSON.stringify(payload)) < MAX_SIZE) {
26 | return true;
27 | }
28 |
29 | let nextConsoleTruncationIdx = 0;
30 | while (isConsoleTelemetryTooBig(payload) && nextConsoleTruncationIdx < payload.console.length) {
31 | payload.console[nextConsoleTruncationIdx].message = truncateString(
32 | payload.console[nextConsoleTruncationIdx].message,
33 | 1000
34 | );
35 | nextConsoleTruncationIdx++;
36 | }
37 |
38 | return true;
39 | }
40 |
--------------------------------------------------------------------------------
/src/handlers/express.ts:
--------------------------------------------------------------------------------
1 | import domain from "domain";
2 | import { AgentRegistrar } from "../AgentRegistrar";
3 | import { uuid } from "../utils/uuid";
4 | import { EventEmitter } from "events";
5 | import { TrackJSEntry } from "../types/TrackJSCapturePayload";
6 |
7 | export type expressMiddleware = (req: any, res: any, next: (error?: any) => void) => void;
8 |
9 | export type expressErrorMiddleware = (error: Error, req: any, res: any, next: (error?: any) => void) => void;
10 |
11 | const DOMAIN_CARRIER_KEY = "__trackjs_domain__";
12 |
13 | function getStatusCode(error) {
14 | const statusCode = error.status || error.statusCode || error.status_code || (error.output && error.output.statusCode);
15 | return statusCode ? parseInt(statusCode, 10) : 500;
16 | }
17 |
18 | /**
19 | * Returns an ExpressJS Request Handler that configures an agent to handle
20 | * events during the request. This should be the *first* handler in the series.
21 | *
22 | * @example
23 | * let app = express()
24 | * .use(TrackJS.expressRequestHandler())
25 | * .use({ all other handlers })
26 | * .listen()
27 | */
28 | export function expressRequestHandler(
29 | options: { correlationHeader: boolean } = { correlationHeader: true }
30 | ): expressMiddleware {
31 | return function trackjsExpressRequestHandler(req, res, next) {
32 | // Creating a NodeJS Error domain for this request, which will allow the
33 | // `AgentRegistrar` the ability to create child agents specific to the
34 | // activities of this request.
35 | const requestDomain = domain.create();
36 |
37 | // Adding the req/res as handlers for domain events. This allows the
38 | // normal error handlers to manage the domain error event as well.
39 | requestDomain.add(req as EventEmitter);
40 | requestDomain.add(res as EventEmitter);
41 | // We could catch the error here, but it would prevent the user from handling
42 | // it themselves. We let it pass and catch using the errorHandler function.
43 | requestDomain.on("error", next);
44 |
45 | // execute the remaining middleware within the context of this domain.
46 | requestDomain.run(() => {
47 | let agent = AgentRegistrar.getCurrentAgent();
48 | req[DOMAIN_CARRIER_KEY] = requestDomain;
49 | let correlationId = uuid();
50 |
51 | // Configure the agent to handle the details of this request.
52 | agent.configure({ correlationId: correlationId }); // correlate all errors from this request together.
53 | agent.environment.start = new Date();
54 | agent.environment.referrerUrl = req["headers"]["referer"] || "";
55 | agent.environment.url = `${req["protocol"]}://${req["get"]("host")}${req["originalUrl"]}`;
56 | if (req["headers"]["user-agent"]) {
57 | agent.metadata.add("__TRACKJS_REQUEST_USER_AGENT", req["headers"]["user-agent"]);
58 | }
59 |
60 | if (options.correlationHeader) {
61 | // We push the current correlationId out as a header so that a TrackJS
62 | // agent on the client-side of the request can link up with us.
63 | res.setHeader("TrackJS-Correlation-Id", correlationId);
64 | res.setHeader("Access-Control-Expose-Headers", "TrackJS-Correlation-Id");
65 | }
66 |
67 | next();
68 | });
69 | };
70 | }
71 |
72 | /**
73 | * Returns an ExpressJS Error Handler that captures errors from processing.
74 | * Should be the *last* handler in the application.
75 | *
76 | * @param options.next {Boolean} True if you want the error passed through to the
77 | * next handler. Default false.
78 | * @example
79 | * let app = express()
80 | * .use({ all other handlers })
81 | * .use(TrackJS.expressErrorHandler({ next: false })) // true if you want to have your own handler after
82 | * .listen()
83 | */
84 | export function expressErrorHandler(options: { next: boolean } = { next: false }): expressErrorMiddleware {
85 | return function trackjsExpressErrorHandler(error, req, res, next) {
86 | if (error && !error["__trackjs__"]) {
87 | var statusCode = getStatusCode(error);
88 | if (statusCode < 500) {
89 | next();
90 | return;
91 | }
92 | AgentRegistrar.getCurrentAgent(req[DOMAIN_CARRIER_KEY]).captureError(error, TrackJSEntry.Express);
93 | }
94 | if (options.next) {
95 | next(error);
96 | }
97 | };
98 | }
99 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | "use awesome";
2 |
3 | import * as sdk from "./sdk";
4 | export const TrackJS = sdk;
5 |
6 | export { TrackJSCapturePayload, TrackJSError, TrackJSInstallOptions, TrackJSOptions } from "./types/index";
7 |
8 | export { RELEASE_NAME as name, RELEASE_HASH as hash, RELEASE_VERSION as version } from "./version";
9 |
--------------------------------------------------------------------------------
/src/sdk.ts:
--------------------------------------------------------------------------------
1 | import { TrackJSInstallOptions, TrackJSOptions, TrackJSError, TrackJSCapturePayload } from "./types";
2 | import { Agent } from "./Agent";
3 | import {
4 | expressRequestHandler,
5 | expressErrorHandler,
6 | expressMiddleware,
7 | expressErrorMiddleware
8 | } from "./handlers/express";
9 | import { ConsoleTelemetry } from "./telemetry";
10 | import { AgentRegistrar } from "./AgentRegistrar";
11 | import { ConsoleWatcher, ExceptionWatcher, RejectionWatcher, Watcher, NetworkWatcher } from "./watchers";
12 | import { isError } from "./utils/isType";
13 | import { serialize } from "./utils/serialize";
14 | import { captureFault } from "./Fault";
15 | import { TrackJSEntry } from "./types/TrackJSCapturePayload";
16 |
17 | let watchers: Array = [ConsoleWatcher, ExceptionWatcher, NetworkWatcher, RejectionWatcher];
18 | let hasInstalled: boolean = false;
19 |
20 | /**
21 | * Whether the agent has been installed into the current environment
22 | *
23 | * @returns {boolean}
24 | */
25 | export function isInstalled(): boolean {
26 | return hasInstalled;
27 | }
28 |
29 | /**
30 | * Install the agent into the current environment.
31 | *
32 | * @param options Installation Options.
33 | */
34 | export function install(options: TrackJSInstallOptions): void {
35 | if (hasInstalled) {
36 | throw new TrackJSError("already installed.");
37 | }
38 | if (!options) {
39 | throw new TrackJSError("install options are required.");
40 | }
41 | if (!options.token) {
42 | throw new TrackJSError("install token is required.");
43 | }
44 |
45 | try {
46 | const agent = new Agent(options);
47 | AgentRegistrar.init(agent);
48 | for (const watcher of watchers) {
49 | watcher.install(agent.options);
50 | }
51 | agent.captureUsage();
52 | hasInstalled = true;
53 | } catch (error) {
54 | captureFault(error);
55 | throw new TrackJSError("error occurred during installation.", error);
56 | }
57 | }
58 |
59 | /**
60 | * Remove the agent from the current environment.
61 | */
62 | export function uninstall(): void {
63 | if (!hasInstalled) {
64 | return;
65 | }
66 |
67 | try {
68 | watchers.forEach((w) => w.uninstall());
69 | AgentRegistrar.close();
70 | hasInstalled = false;
71 | } catch (error) {
72 | captureFault(error);
73 | throw new TrackJSError("error occurred during uninstall.", error);
74 | }
75 | }
76 |
77 | /**
78 | * Update the current agent options from previously installed values.
79 | *
80 | * @param options Options to be updated.
81 | */
82 | export function configure(options: TrackJSOptions): void {
83 | if (!hasInstalled) {
84 | throw new TrackJSError("not installed.");
85 | }
86 | AgentRegistrar.getCurrentAgent().configure(options);
87 | }
88 |
89 | /**
90 | * Add a key-value pair of strings to metadata.
91 | * Or add a Dictionary Object of key-value pairs to metadata.
92 | *
93 | * @param meta {String|Dictionary} key to add, or Dictionary Object.
94 | * @param value {String} value to add.
95 | * @example
96 | * metadata.add('foo', 'bar')
97 | * metadata.add({ 'foo': 'bar', 'bar': 'baz' })
98 | */
99 | export function addMetadata(meta: string | { [key: string]: string }, value?: string): void {
100 | if (!hasInstalled) {
101 | throw new TrackJSError("not installed.");
102 | }
103 | AgentRegistrar.getCurrentAgent().metadata.add(meta, value);
104 | }
105 |
106 | /**
107 | * Remove a key from metadata.
108 | * Or remove a Dictionary Object of keys from metadata. The values in the
109 | * dictionary do not matter.
110 | *
111 | * @param meta {String|Dictionary} key to add, or Dictionary Object.
112 | * @example
113 | * metadata.remove('foo')
114 | * metadata.remove({ 'foo': '', 'bar': '' })
115 | */
116 | export function removeMetadata(meta: string | { [key: string]: string }): void {
117 | if (!hasInstalled) {
118 | throw new TrackJSError("not installed.");
119 | }
120 | AgentRegistrar.getCurrentAgent().metadata.remove(meta);
121 | }
122 |
123 | /**
124 | * Add a message to the Telemetry log.
125 | *
126 | * @param severity {String} "log","debug","info","warn","error"
127 | * @param messages Any messages to be added to the log
128 | */
129 | export function addLogTelemetry(severity: string, ...messages: any): void {
130 | if (!hasInstalled) {
131 | throw new TrackJSError("not installed.");
132 | }
133 | AgentRegistrar.getCurrentAgent().telemetry.add("c", new ConsoleTelemetry(severity, messages));
134 | }
135 |
136 | /**
137 | * For parity with the browser agent, add a set of helper functions that resembles the
138 | * console object hanging off TrackJS.
139 | */
140 | export const console = {
141 | log: (...messages: any): void => addLogTelemetry("log", ...messages),
142 | info: (...messages: any): void => addLogTelemetry("info", ...messages),
143 | debug: (...messages: any): void => addLogTelemetry("debug", ...messages),
144 | warn: (...messages: any): void => addLogTelemetry("warn", ...messages),
145 | error: (...messages: any): void => {
146 | if (!hasInstalled) {
147 | throw new TrackJSError("not installed.");
148 | }
149 | let error = isError(messages) ? messages : new Error(serialize(messages));
150 | AgentRegistrar.getCurrentAgent().captureError(error, TrackJSEntry.Console);
151 | }
152 | };
153 |
154 | /**
155 | * Attach a event handler to Errors. Event handlers will be called in order
156 | * they were attached.
157 | *
158 | * @param func {Function} Event handler that accepts a `TrackJSCapturePayload`.
159 | * Returning `false` from the handler will cause the Error to be ignored.
160 | * @example
161 | * TrackJS.onError((payload) => {
162 | * return (payload.message.indexOf('NSA') >= 0)
163 | * })
164 | */
165 | export function onError(func: (payload: TrackJSCapturePayload) => boolean): void {
166 | if (!hasInstalled) {
167 | throw new TrackJSError("not installed.");
168 | }
169 | AgentRegistrar.getCurrentAgent().onError(func);
170 | }
171 |
172 | /**
173 | * Sends a usage beacon for tracking error rates.
174 | */
175 | export function usage(): void {
176 | if (!hasInstalled) {
177 | throw new TrackJSError("not installed.");
178 | }
179 | AgentRegistrar.getCurrentAgent().captureUsage();
180 | }
181 |
182 | /**
183 | * Track error data.
184 | *
185 | * @param data {*} Data to be tracked to the TrackJS service.
186 | * @param options {TrackJSOptions} Override the installation settings.
187 | * @returns {Error} passed or generated error object.
188 | */
189 | export function track(data: any, options?: TrackJSOptions): Error {
190 | if (!hasInstalled) {
191 | throw new TrackJSError("not installed.");
192 | }
193 | let error = isError(data) ? data : new Error(serialize(data));
194 |
195 | // The user wants to do a one-off track() that overrides agent options
196 | if (options) {
197 | AgentRegistrar.getCurrentAgent()
198 | .clone(options)
199 | .captureError(error, TrackJSEntry.Direct);
200 | } else {
201 | AgentRegistrar.getCurrentAgent().captureError(error, TrackJSEntry.Direct);
202 | }
203 |
204 | return error;
205 | }
206 |
207 | export const Handlers = {
208 | /**
209 | * Returns an ExpressJS Error Handler that captures errors from processing.
210 | * Should be the *last* handler in the application.
211 | *
212 | * @example
213 | * let app = express()
214 | * .use({ all other handlers })
215 | * .use(TrackJS.expressErrorHandler())
216 | * .listen()
217 | */
218 | expressErrorHandler(): expressErrorMiddleware {
219 | if (!hasInstalled) {
220 | throw new TrackJSError("not installed.");
221 | }
222 | return expressErrorHandler();
223 | },
224 |
225 | /**
226 | * Returns an ExpressJS Request Handler that configures an agent to handle
227 | * events during the request. This should be the *first* handler in the series.
228 | *
229 | * @example
230 | * let app = express()
231 | * .use(TrackJS.expressRequestHandler())
232 | * .use({ all other handlers })
233 | * .listen()
234 | */
235 | expressRequestHandler(): expressMiddleware {
236 | if (!hasInstalled) {
237 | throw new TrackJSError("not installed.");
238 | }
239 | return expressRequestHandler();
240 | }
241 | };
242 |
--------------------------------------------------------------------------------
/src/telemetry/ConsoleTelemetry.ts:
--------------------------------------------------------------------------------
1 | import { serialize } from "../utils/serialize";
2 | import { TrackJSConsole } from "../types";
3 |
4 | export class ConsoleTelemetry implements TrackJSConsole {
5 | message: string;
6 | severity: string;
7 | timestamp: string;
8 |
9 | constructor(severity: string, messages: Array) {
10 | this.severity = ConsoleTelemetry.normalizeSeverity(severity);
11 | this.message = serialize(messages.length === 1 ? messages[0] : messages);
12 | this.timestamp = new Date().toISOString();
13 | }
14 |
15 | static normalizeSeverity(severity: string): string {
16 | severity = severity.toLowerCase();
17 | if (["debug", "info", "warn", "error", "log"].indexOf(severity) < 0) {
18 | return "log";
19 | }
20 | return severity;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/telemetry/NetworkTelemetry.ts:
--------------------------------------------------------------------------------
1 | import { TrackJSNetwork } from "../types";
2 |
3 | export class NetworkTelemetry implements TrackJSNetwork {
4 | /** @inheritdoc */
5 | completedOn: string;
6 | /** @inheritdoc */
7 | method: string;
8 | /** @inheritdoc */
9 | startedOn: string;
10 | /** @inheritdoc */
11 | statusCode: number;
12 | /** @inheritdoc */
13 | statusText: string;
14 | /** @inheritdoc */
15 | type: string;
16 | /** @inheritdoc */
17 | url: string;
18 | }
19 |
--------------------------------------------------------------------------------
/src/telemetry/TelemetryBuffer.ts:
--------------------------------------------------------------------------------
1 | interface TelemetryEnvelope {
2 | key: Symbol;
3 | category: string;
4 | data: Object;
5 | }
6 |
7 | /**
8 | * Rotating log of Telemetry data.
9 | */
10 | export class TelemetryBuffer {
11 | private _store: Array;
12 | private _size: number;
13 |
14 | constructor(bufferSize: number, store?: Array) {
15 | this._size = bufferSize;
16 | this._store = store ? store.slice(0) : [];
17 | }
18 |
19 | /**
20 | * Add a log entry with the provided category.
21 | *
22 | * @method add
23 | * @param {String} category The category of the log to be added.
24 | * @param {Object} item The Item to be added to the log
25 | * @returns {Symbol} Id of the item in the log.
26 | */
27 | add(category: string, data: Object): Symbol {
28 | var key = Symbol();
29 | this._store.push({ key, category, data });
30 |
31 | if (this._store.length > this._size) {
32 | this._store = this._store.slice(Math.max(this._store.length - this._size, 0));
33 | }
34 | return key;
35 | }
36 |
37 | /**
38 | * Removes all items from the buffer
39 | *
40 | * @method clear
41 | */
42 | clear(): void {
43 | this._store.length = 0;
44 | }
45 |
46 | /**
47 | * Clone the current buffer into a new instance
48 | *
49 | * @method clone
50 | * @returns {TelemetryBuffer} Clone
51 | */
52 | clone(): TelemetryBuffer {
53 | return new TelemetryBuffer(this._size, this._store);
54 | }
55 |
56 | /**
57 | * Returns the current number of items in the buffer
58 | *
59 | * @method count
60 | * @returns {Number}
61 | */
62 | count(): number {
63 | return this._store.length;
64 | }
65 |
66 | /**
67 | * Gets a telemetry item from the store with the provided key.
68 | *
69 | * @method get
70 | * @param key {Symbol} @see add
71 | * @returns {Object}
72 | */
73 | get(key: Symbol): Object {
74 | let result = this._store.find((envelope) => envelope.key === key);
75 | return result ? result.data : null;
76 | }
77 |
78 | /**
79 | * Returns all the Telemetry of a type.
80 | *
81 | * @method getAllByCategory
82 | * @param {String} category The category of logs to return.
83 | */
84 | getAllByCategory(category: string): Array {
85 | return this._store.filter((envelope) => envelope.category === category).map((envelope) => envelope.data);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/telemetry/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ConsoleTelemetry";
2 | export * from "./NetworkTelemetry";
3 | export * from "./TelemetryBuffer";
4 |
--------------------------------------------------------------------------------
/src/types/TrackJSCapturePayload.ts:
--------------------------------------------------------------------------------
1 | export interface TrackJSCapturePayload {
2 | agentPlatform?: string;
3 | bindStack?: string;
4 | bindTime?: string;
5 | console: Array;
6 | customer: {
7 | application: string;
8 | correlationId?: string;
9 | sessionId: string;
10 | token: string;
11 | userId: string;
12 | version: string;
13 | };
14 | entry: TrackJSEntry;
15 | environment: {
16 | age: number;
17 | dependencies: {
18 | [name: string]: string;
19 | };
20 | originalUrl: string;
21 | referrer: string;
22 | userAgent: string;
23 | viewportHeight?: number;
24 | viewportWidth?: number;
25 | };
26 | file: string;
27 | message: string;
28 | metadata: Array;
29 | nav: Array<{}>;
30 | network: Array;
31 | url: string;
32 | stack: string;
33 | throttled: number;
34 | timestamp: string;
35 | visitor: Array<{}>;
36 | version: string;
37 | }
38 |
39 | export interface TrackJSConsole {
40 | timestamp: string;
41 | severity: string;
42 | message: string;
43 | }
44 |
45 | export interface TrackJSMetadata {
46 | key: string;
47 | value: string;
48 | }
49 |
50 | /**
51 | * TrackJS Network Telemetry Item.
52 | */
53 | export interface TrackJSNetwork {
54 | /**
55 | * Network Request complete time.
56 | * @property {String ISO 8601}
57 | * @example 2000-01-01T12:00:00.000Z
58 | */
59 | completedOn: string;
60 |
61 | /**
62 | * HTTP Method of the request. "GET","POST","UPDATE","DELETE","..."
63 | */
64 | method: string;
65 |
66 | /**
67 | * TrackJS CorrelationId from the agent on the other side of the request. This
68 | * used to link data between client and server agents.
69 | */
70 | requestCorrelationId?: string;
71 |
72 | /**
73 | * Network Request start time.
74 | * @property {String ISO 8601}
75 | * @example 2000-01-01T12:00:00.000Z
76 | */
77 | startedOn: string;
78 |
79 | /**
80 | * HTTP Status code of the completed request
81 | */
82 | statusCode: number;
83 |
84 | /**
85 | * HTTP Status text of the completed request
86 | * @example "Not Found"
87 | */
88 | statusText: string;
89 |
90 | /**
91 | * Network API type watched.
92 | */
93 | type: string;
94 |
95 | /**
96 | * URL destination of the request.
97 | */
98 | url: string;
99 | }
100 |
101 | export enum TrackJSEntry {
102 | Console = "console",
103 | Direct = "direct",
104 | Express = "express",
105 | Global = "global",
106 | Network = "ajax",
107 | Promise = "promise"
108 | }
109 |
--------------------------------------------------------------------------------
/src/types/TrackJSError.ts:
--------------------------------------------------------------------------------
1 | export class TrackJSError extends Error {
2 | innerError: Error;
3 |
4 | constructor(_message: string, _innerError?: Error) {
5 | _message = "TrackJS: " + _message + " See https://docs.trackjs.com/";
6 | super(_message);
7 | this.innerError = _innerError;
8 | Object.setPrototypeOf(this, TrackJSError.prototype);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/types/TrackJSOptions.ts:
--------------------------------------------------------------------------------
1 | import { TrackJSCapturePayload } from "./TrackJSCapturePayload";
2 |
3 | export interface TrackJSInstallOptions extends TrackJSOptions {
4 | /**
5 | * @inheritdoc
6 | */
7 | token: string;
8 | }
9 |
10 | export interface TrackJSOptions {
11 | /**
12 | * Your TrackJS Account Token.
13 | */
14 | token?: string;
15 |
16 | /**
17 | * TrackJS Application Key
18 | */
19 | application?: string;
20 |
21 | /**
22 | * Identifier to correlate errors together share a common thread, session,
23 | * or request.
24 | * @property {String}
25 | */
26 | correlationId?: string;
27 |
28 | /**
29 | * The discovery and inclusion of default environment
30 | * metadata, such as hostname and username.
31 | * @default true
32 | */
33 | defaultMetadata?: boolean;
34 |
35 | /**
36 | * The discovery and inclusion of module dependencies with
37 | * error reports.
38 | * @default true
39 | */
40 | dependencies?: boolean;
41 |
42 | /**
43 | * URL destination override for capturing errors.
44 | */
45 | errorURL?: string;
46 |
47 | /**
48 | * URL destination override for agent fault reports.
49 | */
50 | faultURL?: string;
51 |
52 | /**
53 | * Metadata Key-Value pairs
54 | */
55 | metadata?: { [key: string]: string };
56 |
57 | /**
58 | * Network telemetry options
59 | */
60 | network?: {
61 | /**
62 | * Whether an error should be captured when a network request returns a 400 or greater status code
63 | */
64 | error?: boolean;
65 |
66 | /**
67 | * Whether network requests should automatically be recorded as Telemetry
68 | */
69 | enabled?: boolean;
70 | };
71 |
72 | /**
73 | * Custom callback handler for errors detected.
74 | */
75 | onError?: (payload: TrackJSCapturePayload) => boolean;
76 |
77 | /**
78 | * Custom user-session identifier.
79 | */
80 | sessionId?: string;
81 |
82 | /**
83 | * URL destination override for recording usage.
84 | */
85 | usageURL?: string;
86 |
87 | /**
88 | * Custom user id.
89 | */
90 | userId?: string;
91 |
92 | /**
93 | * Version id of your application.
94 | */
95 | version?: string;
96 | }
97 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export { TrackJSCapturePayload, TrackJSConsole, TrackJSMetadata, TrackJSNetwork } from "./TrackJSCapturePayload";
2 |
3 | export { TrackJSError } from "./TrackJSError";
4 | export { TrackJSInstallOptions, TrackJSOptions } from "./TrackJSOptions";
5 |
--------------------------------------------------------------------------------
/src/utils/cli.ts:
--------------------------------------------------------------------------------
1 | import { exec } from "child_process";
2 |
3 | /**
4 | * Executes a command line argument in a separate thread and returns the results
5 | * of stdout to resolve. Rejected on stderr.
6 | *
7 | * @param command Command string to be executed
8 | */
9 | export function cli(command: string): Promise {
10 | return new Promise((resolve, reject) => {
11 | const args = command.split(" ").splice(1);
12 | const exe = command.split(" ")[0];
13 | const action = exec(command, (error, stdout, stderr) => {
14 | if (error || stderr) {
15 | reject(error || stderr);
16 | } else {
17 | resolve(stdout);
18 | }
19 | });
20 | action.stdout.on("data", resolve);
21 | action.stderr.on("data", reject);
22 | action.on("close", (code) => {
23 | if (code != 0) {
24 | reject(code);
25 | } else {
26 | resolve("code 0");
27 | }
28 | });
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/src/utils/isType.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * getTag
3 | * @private
4 | * Returns the string notation tag of an object, such as `[object Object]`.
5 | *
6 | * @param {*} thing Thing to be checked
7 | * @return {String} tag for the thing.
8 | */
9 | function getTag(thing: any): string {
10 | return Object.prototype.toString.call(thing);
11 | }
12 |
13 | /**
14 | * isArray
15 | * Whether a thing is an Array
16 | *
17 | * @param {*} thing to be checked
18 | * @return {Boolean} true if thing is an Array
19 | */
20 | export function isArray(thing: any): boolean {
21 | return getTag(thing) === "[object Array]";
22 | }
23 |
24 | /**
25 | * isBoolean
26 | * Check if the item is a Boolean
27 | *
28 | * @param {*} thing Item to be checked
29 | * @return {Boolean} Whether the thing was a boolean
30 | */
31 | export function isBoolean(thing: any): boolean {
32 | return typeof thing === "boolean" || (isObject(thing) && getTag(thing) === "[object Boolean]");
33 | }
34 |
35 | /**
36 | * isError
37 | * Checks if the item is an Error
38 | *
39 | * @param {*} thing Item to be checked
40 | * @return {Boolean} Whether item is an Error.
41 | */
42 | export function isError(thing: any): boolean {
43 | if (!isObject(thing)) {
44 | return false;
45 | }
46 | var tag = getTag(thing);
47 | return (
48 | tag === "[object Error]" ||
49 | tag === "[object DOMException]" ||
50 | (isString(thing["name"]) && isString(thing["message"]))
51 | );
52 | }
53 |
54 | /**
55 | * isElement
56 | * Checks if the item is an HTML Element. Because some browsers are not
57 | * compliant with W3 DOM2 specification, `HTMLElement` may not exist or
58 | * be defined inconsistently. Instead, we check if the shape of the object
59 | * is consistent with an Element. This is the same approach used by Lodash.
60 | *
61 | * @param {*} thing Item to be checked
62 | * @return {Boolean} Whether item is an Element.
63 | */
64 | export function isElement(thing: any): boolean {
65 | return isObject(thing) && thing["nodeType"] === 1;
66 | }
67 |
68 | /**
69 | * isFunction
70 | * Whether the provided thing is a Function
71 | *
72 | * @param {*} thing Item to be checked
73 | * @return {Boolean} result
74 | */
75 | export function isFunction(thing: any): boolean {
76 | return !!(thing && typeof thing === "function");
77 | }
78 |
79 | /**
80 | * isNumber
81 | * Whether a thing is an Number
82 | *
83 | * @param {*} thing to be checked
84 | * @return {Boolean} true if thing is an Number
85 | */
86 | export function isNumber(thing: any): boolean {
87 | return typeof thing === "number" || (isObject(thing) && getTag(thing) === "[object Number]");
88 | }
89 |
90 | /**
91 | * isObject
92 | * Checks if the item is an Object
93 | *
94 | * @param {*} thing Item to be checked
95 | * @return {Boolean} Whether item is an object.
96 | */
97 | export function isObject(thing: any): boolean {
98 | return !!(thing && typeof thing === "object");
99 | }
100 |
101 | /**
102 | * isString
103 | * Whether a thing is an String
104 | *
105 | * @param {*} thing to be checked
106 | * @return {Boolean} true if thing is an String
107 | */
108 | export function isString(thing: any): boolean {
109 | return typeof thing === "string" || (!isArray(thing) && isObject(thing) && getTag(thing) === "[object String]");
110 | }
111 |
--------------------------------------------------------------------------------
/src/utils/nestedAssign.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * nestedAssign
3 | * Assigns the properties from the source objects onto the target. Unlike
4 | * object.assign, this follows nested objects and assigns the properties from each
5 | * nested object as well.
6 | *
7 | * @param {target} object Target object
8 | * @param {...sources} object[] Source objects
9 | * @return {object}
10 | */
11 | export function nestedAssign(target: object, ...sources: object[]): object {
12 | for (const source of sources) {
13 | for (const key of Object.keys(source)) {
14 | if (typeof source[key] === "object") {
15 | if (!target[key]) {
16 | // create an empty target nested object so we don't manipulate the sources
17 | Object.assign(target, { [key]: {} });
18 | }
19 | nestedAssign(target[key], source[key]);
20 | } else {
21 | Object.assign(target, { [key]: source[key] });
22 | }
23 | }
24 | }
25 | return target;
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/patch.ts:
--------------------------------------------------------------------------------
1 | const SYM = Symbol("TrackJS Patch");
2 |
3 | /**
4 | * patch
5 | * Monkeypatch a method
6 | *
7 | * @param {Object} obj The object containing the method.
8 | * @param {String} name The name of the method
9 | * @param {Function} func A function to monkeypatch into the method. Will
10 | * be called with the original function as the parameter.
11 | */
12 | export function patch(obj: any, name: string, func: Function): void {
13 | var original = obj[name] || function() {};
14 | obj[name] = func(original);
15 | obj[name][SYM] = original;
16 | }
17 |
18 | /**
19 | * unpatch
20 | * Remove Monkeypatch from a method. Only works with methods patched using
21 | * `patch` from *this* instance.
22 | *
23 | * @param {Object} obj The object containing the method.
24 | * @param {String} name The name of the method
25 | */
26 | export function unpatch(obj: any, name: string): void {
27 | if (obj[name] && obj[name][SYM]) {
28 | var original = obj[name][SYM];
29 | obj[name] = original;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/serialize.ts:
--------------------------------------------------------------------------------
1 | import * as isType from "./isType";
2 |
3 | function serializeElement(el) {
4 | var htmlTagResult = "<" + el.tagName.toLowerCase();
5 | var attributes = el["attributes"] || [];
6 | for (var idx = 0; idx < attributes.length; idx++) {
7 | htmlTagResult += " " + attributes[idx].name + '="' + attributes[idx].value + '"';
8 | }
9 | return htmlTagResult + ">";
10 | }
11 |
12 | /**
13 | * serialize
14 | * Default serializer function that takes an arbitrary value and converts
15 | * it to a string representation.
16 | *
17 | * @param {*} thing Thing to serialize
18 | * @return {String} serialized version of thing.
19 | */
20 | export function serialize(thing: any): string {
21 | if (thing === "") {
22 | return "Empty String";
23 | }
24 | if (thing === undefined) {
25 | return "undefined";
26 | }
27 | if (isType.isString(thing) || isType.isNumber(thing) || isType.isBoolean(thing) || isType.isFunction(thing)) {
28 | return "" + thing;
29 | }
30 | if (isType.isElement(thing)) {
31 | return serializeElement(thing);
32 | }
33 | if (typeof thing === "symbol") {
34 | return Symbol.prototype.toString.call(thing);
35 | }
36 |
37 | var result;
38 | try {
39 | result = JSON.stringify(thing, function(key, value) {
40 | if (value === undefined) {
41 | return "undefined";
42 | }
43 | if (isType.isNumber(value) && isNaN(value)) {
44 | return "NaN";
45 | }
46 | // NOTE [Todd Gardner] Errors do not serialize automatically do to some
47 | // trickery on where the normal properties reside. So let's convert
48 | // it into an object that can be serialized.
49 | if (isType.isError(value)) {
50 | return {
51 | name: value["name"],
52 | message: value["message"],
53 | stack: value["stack"]
54 | };
55 | }
56 | if (isType.isElement(value)) {
57 | return serializeElement(value);
58 | }
59 | return value;
60 | });
61 | } catch (e) {
62 | // NOTE [Todd Gardner] There were circular references inside of the thing
63 | // so let's fallback to a simpler serialization, just the top-level
64 | // keys on the thing, using only string coercion.
65 | var unserializableResult = "";
66 | for (var key in thing) {
67 | if (!thing.hasOwnProperty(key)) {
68 | continue;
69 | }
70 | unserializableResult += ',"' + key + '":"' + thing[key] + '"';
71 | }
72 | result = unserializableResult ? "{" + unserializableResult.replace(",", "") + "}" : "Unserializable Object";
73 | }
74 |
75 | // NOTE [Todd Gardner] in order to correctly capture undefined and NaN,
76 | // we wrote them out as strings. But they are not strings, so let's
77 | // remove the quotes.
78 | return result.replace(/"undefined"/g, "undefined").replace(/"NaN"/g, "NaN");
79 | }
80 |
--------------------------------------------------------------------------------
/src/utils/truncateString.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Truncate a string at a specified length and append ellipsis with a count
3 | * of truncated characters. e.g. "this string was too...{12}".
4 | *
5 | * @param {String} str String to truncate
6 | * @param {Number} length Maximum string length
7 | * @return {String} truncated string.
8 | */
9 | export function truncateString(value: string, length: number): string {
10 | if (value.length <= length) {
11 | return value;
12 | }
13 | var truncatedLength = value.length - length;
14 | return value.substr(0, length) + "...{" + truncatedLength + "}";
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/userAgent.ts:
--------------------------------------------------------------------------------
1 | import os from "os";
2 |
3 | /**
4 | * Made-up UserAgent-like string for our Node Agent.
5 | */
6 | export const userAgent = `node/${process.version} (${os.platform()} ${os.arch()} ${os.release()})`;
7 |
--------------------------------------------------------------------------------
/src/utils/uuid.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 |
3 | /**
4 | * Creates a RFC-compliant v4 uuid string.
5 | * @see https://github.com/kelektiv/node-uuid/
6 | */
7 | export function uuid(): string {
8 | let rng = crypto.randomBytes(16);
9 |
10 | // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
11 | rng[6] = (rng[6] & 0x0f) | 0x40;
12 | rng[8] = (rng[8] & 0x3f) | 0x80;
13 |
14 | let i = 0;
15 | // join used to fix memory issue caused by concatenation: https://bugs.chromium.org/p/v8/issues/detail?id=3175#c4
16 | return [
17 | byteToHex[rng[i++]],
18 | byteToHex[rng[i++]],
19 | byteToHex[rng[i++]],
20 | byteToHex[rng[i++]],
21 | "-",
22 | byteToHex[rng[i++]],
23 | byteToHex[rng[i++]],
24 | "-",
25 | byteToHex[rng[i++]],
26 | byteToHex[rng[i++]],
27 | "-",
28 | byteToHex[rng[i++]],
29 | byteToHex[rng[i++]],
30 | "-",
31 | byteToHex[rng[i++]],
32 | byteToHex[rng[i++]],
33 | byteToHex[rng[i++]],
34 | byteToHex[rng[i++]],
35 | byteToHex[rng[i++]],
36 | byteToHex[rng[i++]]
37 | ].join("");
38 | }
39 |
40 | var byteToHex = [];
41 | for (var i = 0; i < 256; ++i) {
42 | byteToHex[i] = (i + 0x100).toString(16).substr(1);
43 | }
44 |
--------------------------------------------------------------------------------
/src/version.ts:
--------------------------------------------------------------------------------
1 | export const RELEASE_VERSION = "%VERSION%";
2 | export const RELEASE_NAME = "%NAME%";
3 | export const RELEASE_HASH = "%HASH%";
4 |
--------------------------------------------------------------------------------
/src/watchers/ConsoleWatcher.ts:
--------------------------------------------------------------------------------
1 | import { patch, unpatch } from "../utils/patch";
2 | import { ConsoleTelemetry } from "../telemetry";
3 | import { AgentRegistrar } from "../AgentRegistrar";
4 | import { Watcher } from "./Watcher";
5 | import { TrackJSEntry } from "../types/TrackJSCapturePayload";
6 | import { isError } from "../utils/isType";
7 | import { TrackJSOptions } from "../types";
8 |
9 | const CONSOLE_FN_NAMES = ["debug", "info", "warn", "error", "log"];
10 |
11 | class _ConsoleWatcher implements Watcher {
12 | /**
13 | * @inheritdoc
14 | * @param _console {Object} override of the global console object.
15 | */
16 | install(options: TrackJSOptions, _console?: Object): void {
17 | let consoleObj = _console || console;
18 | CONSOLE_FN_NAMES.forEach((name) => {
19 | patch(consoleObj, name, function(originalFn) {
20 | return function(...messages: any) {
21 | let agent = AgentRegistrar.getCurrentAgent();
22 | let data = new ConsoleTelemetry(name, messages);
23 | agent.telemetry.add("c", data);
24 | if (name === "error") {
25 | // When a framework duplicates an error out into `console.error`, we
26 | // don't want to mangle that to allow it to get managed by
27 | // deduplicate.
28 | if (messages.length === 1 && isError(messages[0])) {
29 | agent.captureError(messages[0], TrackJSEntry.Console);
30 | } else {
31 | agent.captureError(new Error(data.message), TrackJSEntry.Console);
32 | }
33 | }
34 | return originalFn.apply(consoleObj, arguments);
35 | };
36 | });
37 | });
38 | }
39 |
40 | /**
41 | * @inheritdoc
42 | * @param _console {Object} override of the global console object.
43 | */
44 | uninstall(_console?: Object): void {
45 | let consoleObj = _console || console;
46 | CONSOLE_FN_NAMES.forEach((name) => unpatch(consoleObj, name));
47 | }
48 | }
49 |
50 | /**
51 | * Watches the global Console for logs.
52 | * Singleton.
53 | */
54 | export const ConsoleWatcher = new _ConsoleWatcher() as Watcher;
55 |
--------------------------------------------------------------------------------
/src/watchers/ExceptionWatcher.ts:
--------------------------------------------------------------------------------
1 | import { AgentRegistrar } from "../AgentRegistrar";
2 | import { Watcher } from ".";
3 | import { TrackJSEntry } from "../types/TrackJSCapturePayload";
4 | import { TrackJSOptions } from "../types";
5 |
6 | class _ExceptionWatcher implements Watcher {
7 | /**
8 | * @inheritdoc
9 | */
10 | install(options: TrackJSOptions): void {
11 | process.on("uncaughtException", this.handleException);
12 | }
13 |
14 | /**
15 | * @inheritdoc
16 | */
17 | uninstall(): void {
18 | process.off("uncaughtException", this.handleException);
19 | }
20 |
21 | handleException(error: Error): void {
22 | AgentRegistrar.getCurrentAgent().captureError(error, TrackJSEntry.Global);
23 | }
24 | }
25 |
26 | /**
27 | * Watches for Global Uncaught Exceptions.
28 | * Singleton.
29 | */
30 | export const ExceptionWatcher = new _ExceptionWatcher() as Watcher;
31 |
--------------------------------------------------------------------------------
/src/watchers/NetworkWatcher.ts:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import https from "https";
3 | import { patch, unpatch } from "../utils/patch";
4 | import { NetworkTelemetry } from "../telemetry";
5 | import { AgentRegistrar } from "../AgentRegistrar";
6 | import { Watcher } from "./Watcher";
7 | import { TrackJSEntry } from "../types/TrackJSCapturePayload";
8 | import { TrackJSOptions } from "../types";
9 |
10 | class _NetworkWatcher implements Watcher {
11 | private options: TrackJSOptions;
12 |
13 | /**
14 | * @inheritdoc
15 | */
16 | install(options: TrackJSOptions): void {
17 | this.options = options;
18 |
19 | if (!this.options.network.enabled) {
20 | return;
21 | }
22 |
23 | const networkWatcher = this;
24 | for (const module of [http, https]) {
25 | for (const method of ["request", "get"]) {
26 | patch(module, method, (original) => {
27 | return function request(options) {
28 | const req = original.apply(this, arguments);
29 |
30 | if (!(options || {}).__trackjs__) {
31 | const networkTelemetry = networkWatcher.createTelemetryFromRequest(req);
32 | AgentRegistrar.getCurrentAgent(req.domain).telemetry.add("n", networkTelemetry);
33 | }
34 |
35 | return req;
36 | };
37 | });
38 | }
39 | }
40 | }
41 |
42 | /**
43 | * @inheritdoc
44 | */
45 | uninstall(): void {
46 | for (const module of [http, https]) {
47 | for (const method of ["request", "get"]) {
48 | unpatch(module, method);
49 | }
50 | }
51 | }
52 |
53 | createTelemetryFromRequest(request: http.ClientRequest): NetworkTelemetry {
54 | let networkTelemetry = new NetworkTelemetry();
55 | networkTelemetry.type = "http";
56 | networkTelemetry.startedOn = new Date().toISOString();
57 | networkTelemetry.method = request["method"];
58 | networkTelemetry.url = `${request["agent"].protocol}//${request.getHeader("host")}${request.path}`;
59 |
60 | request.once("socket", () => {
61 | networkTelemetry.startedOn = new Date().toISOString();
62 | });
63 |
64 | request.once("response", (response: http.IncomingMessage) => {
65 | networkTelemetry.statusCode = response.statusCode;
66 | networkTelemetry.statusText = response.statusMessage;
67 | networkTelemetry.completedOn = new Date().toISOString();
68 |
69 | if (this.options.network.error && networkTelemetry.statusCode >= 400) {
70 | AgentRegistrar.getCurrentAgent(request["domain"]).captureError(
71 | new Error(
72 | `${networkTelemetry.statusCode} ${networkTelemetry.statusText}: ${networkTelemetry.method} ${networkTelemetry.url}`
73 | ),
74 | TrackJSEntry.Network
75 | );
76 | }
77 | });
78 |
79 | return networkTelemetry;
80 | }
81 | }
82 |
83 | /**
84 | * Watches http/s requests for logs.
85 | * Singleton.
86 | */
87 | export const NetworkWatcher = new _NetworkWatcher() as Watcher;
88 |
--------------------------------------------------------------------------------
/src/watchers/RejectionWatcher.ts:
--------------------------------------------------------------------------------
1 | import { AgentRegistrar } from "../AgentRegistrar";
2 | import { Watcher } from ".";
3 | import { isError } from "../utils/isType";
4 | import { serialize } from "../utils/serialize";
5 | import { TrackJSEntry } from "../types/TrackJSCapturePayload";
6 | import { TrackJSOptions } from "../types";
7 |
8 | class _RejectionWatcher implements Watcher {
9 | /**
10 | * @inheritdoc
11 | */
12 | install(options: TrackJSOptions): void {
13 | process.on("unhandledRejection", this.rejectionHandler);
14 | }
15 |
16 | /**
17 | * @inheritdoc
18 | */
19 | uninstall(): void {
20 | process.off("unhandledRejection", this.rejectionHandler);
21 | }
22 |
23 | rejectionHandler(reason: any, promise: any): void {
24 | let error = isError(reason) ? reason : new Error(serialize(reason));
25 | let agent = AgentRegistrar.getCurrentAgent(promise && promise.domain);
26 | agent.captureError(error, TrackJSEntry.Promise);
27 | }
28 | }
29 |
30 | /**
31 | * Watches for Unhandled Promise Rejections.
32 | * Singleton.
33 | */
34 | export const RejectionWatcher = new _RejectionWatcher() as Watcher;
35 |
--------------------------------------------------------------------------------
/src/watchers/Watcher.ts:
--------------------------------------------------------------------------------
1 | import { TrackJSOptions } from "../types";
2 |
3 | /**
4 | * Watches for a particular situation in the environment and handles notifying
5 | * the running agent appropriately.
6 | */
7 | export interface Watcher {
8 | /**
9 | * Install the watcher into the environment.
10 | * @see uninstall
11 | */
12 | install(options: TrackJSOptions): void;
13 |
14 | /**
15 | * Removes the watcher from the environment.
16 | * @see install
17 | */
18 | uninstall(): void;
19 | }
20 |
--------------------------------------------------------------------------------
/src/watchers/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Watcher";
2 | export * from "./ConsoleWatcher";
3 | export * from "./ExceptionWatcher";
4 | export * from "./NetworkWatcher";
5 | export * from "./RejectionWatcher";
6 |
--------------------------------------------------------------------------------
/test/Agent.test.ts:
--------------------------------------------------------------------------------
1 | import os from "os";
2 | import { Agent } from "../src/Agent";
3 | import { ConsoleTelemetry } from "../src/telemetry";
4 | import { transmit } from "../src/Transmitter";
5 | import { TrackJSEntry } from "../src/types/TrackJSCapturePayload";
6 |
7 | jest.mock("../src/Transmitter");
8 |
9 | describe("Agent", () => {
10 | describe("constructor()", () => {
11 | it("initializes with options", () => {
12 | let options = {
13 | token: "token",
14 | application: "application",
15 | correlationId: "correlation",
16 | defaultMetadata: false,
17 | dependencies: false,
18 | errorURL: "https://mycapture.com/",
19 | faultURL: "https://myfault.com/",
20 | network: {
21 | error: true,
22 | enabled: false
23 | },
24 | sessionId: "session",
25 | usageURL: "https://myusage.com/",
26 | userId: "user",
27 | version: "version"
28 | };
29 | let context = new Agent(options);
30 | expect(context.options).toEqual(options);
31 | });
32 |
33 | it("initializes with default options", () => {
34 | const options = {
35 | token: "token"
36 | };
37 | const agent = new Agent(options);
38 | expect(agent.options).toEqual({
39 | token: "token",
40 | application: "",
41 | faultURL: "https://usage.trackjs.com/fault.gif",
42 | errorURL: "https://capture.trackjs.com/capture/node",
43 | defaultMetadata: true,
44 | dependencies: true,
45 | network: {
46 | error: true,
47 | enabled: true
48 | },
49 | sessionId: "",
50 | usageURL: "https://usage.trackjs.com/usage.gif",
51 | userId: "",
52 | version: ""
53 | });
54 | });
55 |
56 | it("initializes metadata", () => {
57 | let agent = new Agent({ token: "test", defaultMetadata: false, metadata: { foo: "bar" } });
58 | expect(agent.metadata.get()).toEqual([{ key: "foo", value: "bar" }]);
59 | });
60 |
61 | it("initializes default metadata", () => {
62 | let agent = new Agent({ token: "test", defaultMetadata: true });
63 | expect(agent.metadata.get()).toEqual(
64 | expect.arrayContaining([
65 | { key: "hostname", value: os.hostname() },
66 | { key: "username", value: os.userInfo().username },
67 | { key: "cwd", value: process.cwd() }
68 | ])
69 | );
70 | });
71 | });
72 |
73 | describe("clone", () => {
74 | it("returns a different agent", () => {
75 | let agent1 = new Agent({ token: "test" });
76 | let agent2 = agent1.clone();
77 | expect(agent1).not.toBe(agent2);
78 | });
79 | it("has equal options", () => {
80 | let agent1 = new Agent({ token: "test" });
81 | let agent2 = agent1.clone();
82 | expect(agent1.options).toEqual(agent2.options);
83 | expect(agent1.options).not.toBe(agent2.options);
84 | });
85 | it("has equal meta", () => {
86 | let agent1 = new Agent({ token: "test" });
87 | agent1.metadata.add("foo", "bar");
88 | let agent2 = agent1.clone();
89 | expect(agent1.metadata.get()).toEqual(agent2.metadata.get());
90 | });
91 | it("has equal telemetry", () => {
92 | let agent1 = new Agent({ token: "test" });
93 | agent1.telemetry.add("t", new ConsoleTelemetry("log", ["message"]));
94 | let agent2 = agent1.clone();
95 | expect(agent1.telemetry.getAllByCategory("t")).toEqual(agent2.telemetry.getAllByCategory("t"));
96 | });
97 | it("has equal environment", () => {
98 | let agent1 = new Agent({ token: "test" });
99 | agent1.environment.url = "http://example.com";
100 | let agent2 = agent1.clone();
101 | expect(agent1.environment).toEqual(agent2.environment);
102 | expect(agent1.environment).not.toBe(agent2.environment);
103 | });
104 | it("has same event handlers", () => {
105 | let agent1 = new Agent({ token: "test" });
106 | let handler1 = jest.fn((payload) => true);
107 | let handler2 = jest.fn((payload) => false);
108 | agent1.onError(handler1);
109 | agent1.onError(handler2);
110 | let agent2 = agent1.clone();
111 | agent2.captureError(new Error("test"), TrackJSEntry.Direct);
112 | expect(handler1).toHaveBeenCalled();
113 | expect(handler2).toHaveBeenCalled();
114 | });
115 | it("clones with options", () => {
116 | let agent1 = new Agent({ token: "test" });
117 | let agent2 = agent1.clone({ application: "app" });
118 | expect(agent2.options).toEqual(
119 | expect.objectContaining({
120 | token: "test",
121 | application: "app"
122 | })
123 | );
124 | });
125 | it("clones with metadata", () => {
126 | let agent1 = new Agent({ token: "test", defaultMetadata: false });
127 | let agent2 = agent1.clone({ metadata: { foo: "bar" } });
128 | expect(agent2.metadata.get()).toEqual([{ key: "foo", value: "bar" }]);
129 | });
130 | });
131 |
132 | describe("createErrorReport()", () => {
133 | it("adds environment properties to payloads", () => {
134 | let agent = new Agent({ token: "test" });
135 | agent.environment.start = new Date(new Date().getTime() - 1);
136 | agent.environment.url = "http://example.com/path?foo=bar";
137 | agent.environment.referrerUrl = "http://test.com/path?bar=baz";
138 | agent.environment.userAgent = "user agent";
139 | let report = agent.createErrorReport(new Error("test"), TrackJSEntry.Direct);
140 | expect(report).toEqual(
141 | expect.objectContaining({
142 | environment: expect.objectContaining({
143 | age: expect.any(Number),
144 | originalUrl: "http://example.com/path?foo=bar",
145 | referrer: "http://test.com/path?bar=baz",
146 | userAgent: "user agent"
147 | }),
148 | url: "http://example.com/path?foo=bar"
149 | })
150 | );
151 | expect(report.environment.age).toBeLessThan(5);
152 | expect(report.environment.age).toBeGreaterThanOrEqual(1);
153 | });
154 |
155 | it("adds metadata to payloads", () => {
156 | let agent = new Agent({ token: "test", defaultMetadata: false });
157 | agent.metadata.add("foo", "bar");
158 | let report = agent.createErrorReport(new Error("test"), TrackJSEntry.Direct);
159 | expect(report).toEqual(
160 | expect.objectContaining({
161 | metadata: [{ key: "foo", value: "bar" }]
162 | })
163 | );
164 | });
165 |
166 | it("adds console telemetry to payload", () => {
167 | let agent = new Agent({ token: "test" });
168 | agent.telemetry.add("c", new ConsoleTelemetry("log", ["a log message"]));
169 | agent.telemetry.add("c", new ConsoleTelemetry("warn", ["a warning", { foo: "bar" }]));
170 | let payload = agent.createErrorReport(new Error("test error"), TrackJSEntry.Direct);
171 | expect(payload.console).toEqual([
172 | {
173 | severity: "log",
174 | message: "a log message",
175 | timestamp: expect.any(String)
176 | },
177 | {
178 | severity: "warn",
179 | message: '["a warning",{"foo":"bar"}]',
180 | timestamp: expect.any(String)
181 | }
182 | ]);
183 | });
184 | });
185 |
186 | describe("onError", () => {
187 | it("receives error events", () => {
188 | let agent = new Agent({ token: "test " });
189 | expect.assertions(1);
190 | agent.onError((payload) => {
191 | expect(payload.message).toBe("test message");
192 | return false;
193 | });
194 | agent.captureError(new Error("test message"), TrackJSEntry.Direct);
195 | });
196 |
197 | it("can ignore error events", () => {
198 | let agent = new Agent({ token: "test " });
199 | expect.assertions(1);
200 | agent.onError((payload) => false);
201 | expect(agent.captureError(new Error("test message"), TrackJSEntry.Direct)).toBe(false);
202 | });
203 |
204 | it("handles multiple callbacks", () => {
205 | let agent = new Agent({ token: "test " });
206 | let cb1 = jest.fn(() => true);
207 | let cb2 = jest.fn(() => true);
208 |
209 | agent.onError(cb1);
210 | agent.onError(cb2);
211 | agent.captureError(new Error("test message"), TrackJSEntry.Direct);
212 |
213 | expect(cb1).toHaveBeenCalled();
214 | expect(cb2).toHaveBeenCalled();
215 | });
216 |
217 | it("stops callbacks once ignored", () => {
218 | let agent = new Agent({ token: "test " });
219 | let cb1 = jest.fn(() => true);
220 | let cb2 = jest.fn(() => false);
221 | let cb3 = jest.fn(() => true);
222 |
223 | agent.onError(cb1);
224 | agent.onError(cb2);
225 | agent.onError(cb3);
226 | agent.captureError(new Error("test message"), TrackJSEntry.Direct);
227 |
228 | expect(cb1).toHaveBeenCalled();
229 | expect(cb2).toHaveBeenCalled();
230 | expect(cb3).not.toHaveBeenCalled();
231 | });
232 |
233 | it("adds handler from constructor", () => {
234 | let handler = jest.fn((payload) => false);
235 | let options = {
236 | token: "token",
237 | onError: handler
238 | };
239 | let agent = new Agent(options);
240 | agent.captureError(new Error("test message"), TrackJSEntry.Direct);
241 | expect(handler).toHaveBeenCalled();
242 | });
243 |
244 | it("recovers from a handler that throws", () => {
245 | let handler = jest.fn((payload) => {
246 | throw new Error("oops");
247 | });
248 | let agent = new Agent({ token: "test" });
249 | agent.onError(handler);
250 | expect(agent.captureError(new Error("test"), TrackJSEntry.Direct)).toBe(true);
251 | expect(transmit).toHaveBeenCalled();
252 | });
253 | });
254 | });
255 |
--------------------------------------------------------------------------------
/test/Environment.test.ts:
--------------------------------------------------------------------------------
1 | import { Environment } from "../src/Environment";
2 |
3 | describe("Environment", () => {
4 | describe("clone()", () => {
5 | it("creates an equal object", () => {
6 | let environment1 = new Environment();
7 | environment1.url = "https://example.com";
8 | let environment2 = environment1.clone();
9 | expect(environment1).toEqual(environment2);
10 | expect(environment1).not.toBe(environment2);
11 | });
12 | });
13 | describe("discoverDependencies()", () => {
14 | it("reuses dependencies between instances", () => {
15 | let environment1 = new Environment();
16 | let environment2 = new Environment();
17 |
18 | environment1.discoverDependencies();
19 | expect(Environment.dependencyCache).not.toBeUndefined();
20 | let dependencyCache = Environment.dependencyCache;
21 |
22 | environment2.discoverDependencies();
23 | expect(Environment.dependencyCache).toStrictEqual(dependencyCache);
24 | });
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/test/Metadata.test.ts:
--------------------------------------------------------------------------------
1 | import { Metadata } from "../src/Metadata";
2 |
3 | describe("Metadata", () => {
4 | describe("constructor()", () => {
5 | it("creates empty", () => {
6 | let m = new Metadata();
7 | expect(m.get()).toEqual([]);
8 | });
9 | it("creates with initial values", () => {
10 | let m = new Metadata({ foo: "bar" });
11 | expect(m.get()).toEqual([
12 | {
13 | key: "foo",
14 | value: "bar"
15 | }
16 | ]);
17 | });
18 | });
19 |
20 | describe("clone()", () => {
21 | it("has equal values", () => {
22 | let meta1 = new Metadata({ foo: "bar" });
23 | let meta2 = meta1.clone();
24 | expect(meta1.get()).toEqual(meta2.get());
25 | expect(meta1.get()).not.toBe(meta2.get());
26 | });
27 | it("can be changed separately", () => {
28 | let meta1 = new Metadata({ foo: "bar" });
29 | let meta2 = meta1.clone();
30 | meta2.add("bar", "baz");
31 | expect(meta1.get()).not.toEqual(meta2.get());
32 | });
33 | });
34 |
35 | describe("add(), remove(), get()", () => {
36 | it("adds values", () => {
37 | let m = new Metadata();
38 | m.add("foo", "bar");
39 | expect(m.get()).toEqual([
40 | {
41 | key: "foo",
42 | value: "bar"
43 | }
44 | ]);
45 | });
46 | it("overwrites values", () => {
47 | let m = new Metadata();
48 | m.add("foo", "bar");
49 | m.add("foo", "baz");
50 | expect(m.get()).toEqual([
51 | {
52 | key: "foo",
53 | value: "baz"
54 | }
55 | ]);
56 | });
57 | it("adds multiple values", () => {
58 | let m = new Metadata();
59 | m.add("foo", "bar");
60 | m.add({
61 | foo: "baz",
62 | bar: "baz"
63 | });
64 | expect(m.get()).toEqual([{ key: "foo", value: "baz" }, { key: "bar", value: "baz" }]);
65 | });
66 | it("adds non-string values", () => {
67 | let m = new Metadata();
68 | m.add({
69 | 4: ({ foo: "bar" } as unknown) as string
70 | });
71 | expect(m.get()).toEqual([
72 | {
73 | key: "4",
74 | value: '{"foo":"bar"}'
75 | }
76 | ]);
77 | });
78 | it("removes values", () => {
79 | let m = new Metadata();
80 | m.add("foo", "bar");
81 | m.remove("foo");
82 | expect(m.get()).toEqual([]);
83 | });
84 | it("removes multiple values", () => {
85 | let m = new Metadata();
86 | m.add("foo", "bar");
87 | m.add("bar", "bar");
88 | m.add("baz", "bar");
89 | m.remove({
90 | foo: "",
91 | baz: ""
92 | });
93 | expect(m.get()).toEqual([{ key: "bar", value: "bar" }]);
94 | });
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/test/agentHelpers/deduplicate.test.ts:
--------------------------------------------------------------------------------
1 | import { deduplicate } from "../../src/agentHelpers/deduplicate";
2 | import { Agent } from "../../src/Agent";
3 | import { TrackJSEntry } from "../../dist/types/TrackJSCapturePayload";
4 |
5 | jest.useFakeTimers();
6 |
7 | beforeAll(() => {
8 | Agent.defaults.dependencies = false;
9 | });
10 |
11 | describe("deduplicate()", () => {
12 | test("it prevents duplicate errors", () => {
13 | let error = new Error("test");
14 | let agent = new Agent({ token: "test" });
15 |
16 | expect(deduplicate(agent.createErrorReport(error, TrackJSEntry.Direct))).toBe(true);
17 | expect(deduplicate(agent.createErrorReport(error, TrackJSEntry.Direct))).toBe(false);
18 | });
19 |
20 | test("does not prevent errors with different data", () => {
21 | let error1 = new Error("test 1");
22 | let error2 = new Error("test 2");
23 | let agent = new Agent({ token: "test" });
24 |
25 | expect(deduplicate(agent.createErrorReport(error1, TrackJSEntry.Direct))).toBe(true);
26 | expect(deduplicate(agent.createErrorReport(error2, TrackJSEntry.Direct))).toBe(true);
27 | });
28 |
29 | test("does not prevent errors after timeout", () => {
30 | let error = new Error("test");
31 | let agent = new Agent({ token: "test" });
32 |
33 | expect(deduplicate(agent.createErrorReport(error, TrackJSEntry.Direct))).toBe(true);
34 | jest.runAllTimers();
35 | expect(deduplicate(agent.createErrorReport(error, TrackJSEntry.Direct))).toBe(true);
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/test/agentHelpers/truncate.test.ts:
--------------------------------------------------------------------------------
1 | import { truncate } from "../../src/agentHelpers";
2 | import { Agent } from "../../src/Agent";
3 | import { TrackJSEntry } from "../../src/types/TrackJSCapturePayload";
4 |
5 | describe("truncate()", () => {
6 | beforeAll(() => {
7 | Agent.defaults.dependencies = false;
8 | });
9 |
10 | function generateRandomString(length: number) {
11 | const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
12 | let result = "";
13 | for (var i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)];
14 | return result;
15 | }
16 |
17 | test("it does not change normal sized payload", () => {
18 | let agent = new Agent({ token: "test" });
19 | for (var i = 0; i < 10; i++) {
20 | agent.telemetry.add("c", { message: generateRandomString(1000) });
21 | }
22 | let payload = agent.createErrorReport(new Error("test"), TrackJSEntry.Direct);
23 | expect(truncate(payload)).toBe(true);
24 | expect(payload.console.length).toBe(10);
25 | payload.console.forEach((c) => {
26 | expect(c.message.length).toBe(1000);
27 | });
28 | });
29 |
30 | test("it truncates oversized payload", () => {
31 | let agent = new Agent({ token: "test" });
32 | for (var i = 0; i < 10; i++) {
33 | agent.telemetry.add("c", { message: generateRandomString(100000) });
34 | }
35 | let payload = agent.createErrorReport(new Error("test"), TrackJSEntry.Direct);
36 | expect(truncate(payload)).toBe(true);
37 | expect(payload.console.length).toBe(10);
38 | payload.console.forEach((c) => {
39 | expect(c.message.length).toBe(1010); // a bit more to handle the ...{n}
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/test/handlers/express.test.ts:
--------------------------------------------------------------------------------
1 | import domain from "domain";
2 | import http from "http";
3 | import { expressErrorHandler, expressRequestHandler } from "../../src/handlers/express";
4 | import { Socket } from "net";
5 | import { Agent } from "../../src/Agent";
6 | import { AgentRegistrar } from "../../src/AgentRegistrar";
7 |
8 | beforeAll(() => {
9 | Agent.defaults.dependencies = false;
10 | });
11 |
12 | describe("expressRequestHandler", () => {
13 | let fakeReq, fakeRes;
14 | let fakeAgent;
15 |
16 | beforeEach(() => {
17 | fakeReq = new http.IncomingMessage(new Socket());
18 | fakeReq["get"] = jest.fn();
19 | fakeRes = new http.ServerResponse(fakeReq);
20 | fakeAgent = new Agent({ token: "test" });
21 | AgentRegistrar.getCurrentAgent = jest.fn(() => fakeAgent);
22 | });
23 |
24 | it("creates and enters domain context", () => {
25 | expect.assertions(2);
26 | expect(domain["active"]).toBe(null);
27 | expressRequestHandler()(fakeReq, fakeRes, () => {
28 | expect(domain["active"]).toBeDefined();
29 | });
30 | });
31 |
32 | it("sets request environment parameters", () => {
33 | fakeReq.headers["referer"] = "https://referer.com";
34 | fakeReq["protocol"] = "https";
35 | fakeReq["get"] = jest.fn(() => "example.com");
36 | fakeReq["originalUrl"] = "/";
37 |
38 | expressRequestHandler()(fakeReq, fakeRes, () => {
39 | expect(fakeAgent.environment.referrerUrl).toBe("https://referer.com");
40 | expect(fakeAgent.environment.url).toBe("https://example.com/");
41 | });
42 | });
43 |
44 | it("sets correlation headers on response", () => {
45 | expressRequestHandler({ correlationHeader: true })(fakeReq, fakeRes, () => {
46 | expect(fakeRes.hasHeader("TrackJS-Correlation-Id")).toBe(true);
47 | expect(fakeRes.getHeader("Access-Control-Expose-Headers")).toBe("TrackJS-Correlation-Id");
48 | });
49 | });
50 | });
51 |
52 | describe("expressErrorHandler", () => {
53 | let fakeReq, fakeRes;
54 | let fakeAgent;
55 |
56 | beforeEach(() => {
57 | fakeReq = new http.IncomingMessage(new Socket());
58 | fakeRes = new http.ServerResponse(fakeReq);
59 | fakeAgent = new Agent({ token: "test" });
60 | AgentRegistrar.getCurrentAgent = jest.fn(() => fakeAgent);
61 | });
62 |
63 | it("captures errors with status code", () => {
64 | let error = new Error("test");
65 | error["statusCode"] = 503;
66 | let next = jest.fn();
67 | jest.spyOn(fakeAgent, "captureError");
68 | expressErrorHandler()(error, fakeReq, fakeRes, next);
69 | expect(fakeAgent.captureError).toHaveBeenCalledWith(error, "express");
70 | });
71 |
72 | it("passes error to next", () => {
73 | let error = new Error("test");
74 | let next = jest.fn();
75 | expressErrorHandler({ next: true })(error, fakeReq, fakeRes, next);
76 | expect(next).toHaveBeenCalledWith(error);
77 | });
78 |
79 | it("does not capture duplicate errors", () => {
80 | let error = new Error("test");
81 | error["statusCode"] = 503;
82 | let next = jest.fn();
83 | jest.spyOn(fakeAgent, "captureError");
84 | expressErrorHandler()(error, fakeReq, fakeRes, next);
85 | expressErrorHandler()(error, fakeReq, fakeRes, next);
86 | expect(fakeAgent.captureError).toHaveBeenCalledTimes(1);
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/test/index.test.ts:
--------------------------------------------------------------------------------
1 | import { TrackJS, TrackJSCapturePayload, TrackJSError, TrackJSOptions } from "../src/index";
2 |
3 | describe("TrackJS", () => {
4 | test("api exists", () => {
5 | expect(TrackJS.install).toEqual(expect.any(Function));
6 | expect(TrackJS.uninstall).toEqual(expect.any(Function));
7 | expect(TrackJS.addMetadata).toEqual(expect.any(Function));
8 | expect(TrackJS.removeMetadata).toEqual(expect.any(Function));
9 | expect(TrackJS.addLogTelemetry).toEqual(expect.any(Function));
10 | expect(TrackJS.onError).toEqual(expect.any(Function));
11 | expect(TrackJS.track).toEqual(expect.any(Function));
12 |
13 | expect(TrackJS.Handlers.expressErrorHandler).toEqual(expect.any(Function));
14 |
15 | expect(TrackJSError).toBeTruthy();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/test/integration/express/index.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 | const express = require('express');
3 | const { TrackJS } = require('../../../dist');
4 |
5 | console.log('Starting ExpressJS Test...');
6 |
7 | const TESTS_EXPECTED = 5;
8 | let testsComplete = 0;
9 |
10 | function testComplete(name) {
11 | testsComplete++;
12 | console.log(`Test ${name} PASSED`);
13 | if (testsComplete >= TESTS_EXPECTED) {
14 | console.log('ExpressJS Tests PASSED');
15 | process.exit(0);
16 | }
17 | }
18 | function assertStrictEqual(thing1, thing2) {
19 | if (thing1 !== thing2) {
20 | console.log("Assertion strict equal failed", thing1, thing2, new Error().stack);
21 | process.exit(1);
22 | }
23 | }
24 |
25 | setTimeout(() => {
26 | console.log("Test timed out");
27 | process.exit(1);
28 | }, 1000*15);
29 |
30 | TrackJS.install({
31 | token: '8de4c78a3ec64020ab2ad15dea1ae9ff',
32 | defaultMetadata: false,
33 | onError: function(payload) {
34 | switch(payload.url) {
35 | case 'http://localhost:3001/sync':
36 | assertStrictEqual(payload.url, 'http://localhost:3001/sync');
37 | assertStrictEqual(payload.message, 'sync blew up');
38 | assertStrictEqual(payload.entry, 'express');
39 | assertStrictEqual(payload.console.length, 1);
40 | assertStrictEqual(payload.console[0].message, 'a message from /sync');
41 | assertStrictEqual(payload.metadata.length, 2);
42 | assertStrictEqual(payload.metadata[0].key, 'test');
43 | assertStrictEqual(payload.metadata[0].value, 'express');
44 | assertStrictEqual(payload.metadata[1].key, 'action');
45 | assertStrictEqual(payload.metadata[1].value, 'sync');
46 | break;
47 | case 'http://localhost:3001/async':
48 | assertStrictEqual(payload.url, 'http://localhost:3001/async');
49 | assertStrictEqual(payload.message, 'async blew up');
50 | assertStrictEqual(payload.entry, 'express');
51 | assertStrictEqual(payload.console.length, 1);
52 | assertStrictEqual(payload.console[0].message, 'a message from /async');
53 | assertStrictEqual(payload.metadata.length, 2);
54 | assertStrictEqual(payload.metadata[0].key, 'test');
55 | assertStrictEqual(payload.metadata[0].value, 'express');
56 | assertStrictEqual(payload.metadata[1].key, 'action');
57 | assertStrictEqual(payload.metadata[1].value, 'async');
58 | break;
59 | case 'http://localhost:3001/reject':
60 | assertStrictEqual(payload.url, 'http://localhost:3001/reject');
61 | assertStrictEqual(payload.message, 'rejected!');
62 | assertStrictEqual(payload.entry, 'express');
63 | assertStrictEqual(payload.console.length, 1);
64 | assertStrictEqual(payload.console[0].message, 'a message from /reject');
65 | assertStrictEqual(payload.metadata.length, 2);
66 | assertStrictEqual(payload.metadata[0].key, 'test');
67 | assertStrictEqual(payload.metadata[0].value, 'express');
68 | assertStrictEqual(payload.metadata[1].key, 'action');
69 | assertStrictEqual(payload.metadata[1].value, 'reject');
70 | break;
71 | case 'http://localhost:3001/console':
72 | assertStrictEqual(payload.url, 'http://localhost:3001/console');
73 | assertStrictEqual(payload.message, 'console blew up');
74 | assertStrictEqual(payload.entry, 'console');
75 | assertStrictEqual(payload.console.length, 1);
76 | assertStrictEqual(payload.console[0].message, 'console blew up');
77 | break;
78 | case 'http://localhost:3001/headers':
79 | assertStrictEqual(payload.url, 'http://localhost:3001/headers');
80 | assertStrictEqual(payload.message, 'checking headers');
81 | assertStrictEqual(payload.entry, 'express');
82 | assertStrictEqual(payload.metadata.length, 2);
83 | assertStrictEqual(payload.metadata[1].key, '__TRACKJS_REQUEST_USER_AGENT');
84 | assertStrictEqual(payload.metadata[1].value, 'test user agent');
85 | break;
86 | default:
87 | console.log('unknown url error', payload);
88 | process.exit(1);
89 | }
90 |
91 | testComplete(payload.url);
92 | return false;
93 | }
94 | });
95 |
96 | TrackJS.addMetadata('test', 'express');
97 |
98 | express()
99 | .use(TrackJS.Handlers.expressRequestHandler())
100 |
101 | .get('/sync', (req, res, next) => {
102 | TrackJS.addLogTelemetry('log', 'a message from /sync');
103 | TrackJS.addMetadata('action', 'sync');
104 | throw new Error('sync blew up');
105 | })
106 |
107 | .get('/async', (req, res, next) => {
108 | TrackJS.addLogTelemetry('log', 'a message from /async');
109 | setTimeout(() => {
110 | TrackJS.addMetadata('action', 'async');
111 | throw new Error('async blew up');
112 | }, 100);
113 | })
114 |
115 | .get('/reject', (req, res, next) => {
116 | TrackJS.addLogTelemetry('log', 'a message from /reject');
117 | new Promise((resolve, reject) => {
118 | TrackJS.addMetadata('action', 'reject');
119 | setTimeout(() => {
120 | reject('rejected!');
121 | }, 100);
122 | })
123 | })
124 |
125 | .get('/console', (req, res, next) => {
126 | console.error('console blew up');
127 | res.sendStatus(200);
128 | })
129 |
130 | .get('/headers', (req, res, next) => {
131 | throw new Error('checking headers');
132 | })
133 |
134 | .get('/ok', (req, res, next) => {
135 | TrackJS.addLogTelemetry('log', 'a message from /ok');
136 | TrackJS.addMetadata('action', 'ok');
137 | res.sendStatus(200);
138 | })
139 |
140 | .use(TrackJS.Handlers.expressErrorHandler({ next: true }))
141 |
142 | .use((error, req, res, next) => {
143 | if (!error['__trackjs__']) {
144 | console.log('UNCAUGHT EXPRESS ERROR', error);
145 | process.exit(1);
146 | }
147 | })
148 |
149 | .listen(3001);
150 |
151 | process.on('uncaughtException', function(error) {
152 | if (!error['__trackjs__']) {
153 | console.log('UNCAUGHT PROCESS ERROR', error);
154 | process.exit(1);
155 | }
156 | });
157 |
158 | // TESTS
159 |
160 | http.get('http://localhost:3001/async');
161 |
162 | // NOTE [Todd] Rejected promises lose their context of what request was running since Node 12.0.0
163 | // due to a bug in unhandledRejection. This prevents the agent from finding the active context,
164 | // and defaults through to the primaryAgent. Fixed in version 15.3
165 | // @see https://github.com/nodejs/node/issues/26794
166 | // @see https://github.com/nodejs/node/issues/29051
167 | // http.get('http://localhost:3001/reject');
168 |
169 | http.get('http://localhost:3001/sync');
170 | http.get('http://localhost:3001/console');
171 | http.get({
172 | hostname: 'localhost',
173 | port: 3001,
174 | path: '/headers',
175 | headers: {
176 | 'user-agent': 'test user agent'
177 | }
178 | });
179 |
180 | // test that our correlation headers are attached
181 | http.get('http://localhost:3001/ok', (res) => {
182 | console.log('returned correlationId header', res.headers['trackjs-correlation-id']);
183 | assertStrictEqual(!!res.headers['trackjs-correlation-id'], true);
184 | testComplete("correlation headers");
185 | });
186 |
--------------------------------------------------------------------------------
/test/integration/network/index.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 | const https = require('https');
3 | const request = require('request');
4 | const axios = require('axios');
5 |
6 | const domain = require('domain');
7 |
8 | const { TrackJS } = require('../../../dist');
9 |
10 | console.log('Starting Networking Test...');
11 |
12 | const TESTS_EXPECTED = 6;
13 | let testsComplete = 0;
14 |
15 | function testComplete() {
16 | testsComplete++;
17 | if (testsComplete >= TESTS_EXPECTED) {
18 | console.log('Networking Tests PASSED');
19 | process.exit(0);
20 | }
21 | }
22 | function assertStrictEqual(thing1, thing2) {
23 | if (thing1 !== thing2) {
24 | console.log("Assertion strict equal failed", thing1, thing2, new Error().stack);
25 | process.exit(1);
26 | }
27 | }
28 |
29 | TrackJS.install({
30 | token: '8de4c78a3ec64020ab2ad15dea1ae9ff',
31 | onError: (payload) => {
32 | switch(payload.customer.userId) {
33 | case 'http':
34 | assertStrictEqual(payload.message, '503 Service Unavailable: GET http://httpstat.us/503');
35 | assertStrictEqual(payload.entry, 'ajax');
36 | assertStrictEqual(payload.console.length, 0);
37 | assertStrictEqual(payload.network.length, 1);
38 | assertStrictEqual(payload.network[0].statusCode, 503);
39 | assertStrictEqual(payload.network[0].type, 'http');
40 | console.log('http request PASSED');
41 | break;
42 | case 'https':
43 | assertStrictEqual(payload.message, '404 Not Found: GET https://httpstat.us/404');
44 | assertStrictEqual(payload.entry, 'ajax');
45 | assertStrictEqual(payload.console.length, 0);
46 | assertStrictEqual(payload.network.length, 1);
47 | assertStrictEqual(payload.network[0].statusCode, 404);
48 | assertStrictEqual(payload.network[0].type, 'http');
49 | console.log('https request PASSED');
50 | break;
51 | case 'http.get':
52 | assertStrictEqual(payload.message, '502 Bad Gateway: GET http://httpstat.us/502');
53 | assertStrictEqual(payload.entry, 'ajax');
54 | assertStrictEqual(payload.console.length, 0);
55 | assertStrictEqual(payload.network.length, 1);
56 | assertStrictEqual(payload.network[0].statusCode, 502);
57 | assertStrictEqual(payload.network[0].type, 'http');
58 | console.log('http.get request PASSED');
59 | break;
60 | case 'https.get':
61 | assertStrictEqual(payload.message, '403 Forbidden: GET https://httpstat.us/403');
62 | assertStrictEqual(payload.entry, 'ajax');
63 | assertStrictEqual(payload.console.length, 0);
64 | assertStrictEqual(payload.network.length, 1);
65 | assertStrictEqual(payload.network[0].statusCode, 403);
66 | assertStrictEqual(payload.network[0].type, 'http');
67 | console.log('https.get request PASSED');
68 | break;
69 | case 'request':
70 | assertStrictEqual(payload.message, '501 Not Implemented: GET https://httpstat.us/501');
71 | assertStrictEqual(payload.entry, 'ajax');
72 | assertStrictEqual(payload.console.length, 0);
73 | assertStrictEqual(payload.network.length, 1);
74 | assertStrictEqual(payload.network[0].statusCode, 501);
75 | assertStrictEqual(payload.network[0].type, 'http');
76 | console.log('request PASSED');
77 | break;
78 | case 'axios':
79 | assertStrictEqual(payload.message, '401 Unauthorized: GET https://httpstat.us/401');
80 | assertStrictEqual(payload.entry, 'ajax');
81 | assertStrictEqual(payload.console.length, 0);
82 | assertStrictEqual(payload.network.length, 1);
83 | assertStrictEqual(payload.network[0].statusCode, 401);
84 | assertStrictEqual(payload.network[0].type, 'http');
85 | console.log('Axoim request PASSED');
86 | break;
87 | default:
88 | console.log('unknown userId error', payload);
89 | process.exit(1);
90 | }
91 |
92 | testComplete();
93 | return false;
94 | }
95 | });
96 |
97 | process.on('uncaughtException', (error) => {
98 | if (!error['__trackjs__']) {
99 | console.log('UNCAUGHT PROCESS ERROR', error);
100 | process.exit(1);
101 | }
102 | });
103 |
104 | domain.create('http.get').run(() => {
105 | TrackJS.configure({ userId: 'http.get' });
106 | http.get('http://httpstat.us/502');
107 | });
108 |
109 | domain.create('https.get').run(() => {
110 | TrackJS.configure({ userId: 'https.get' });
111 | https.get('https://httpstat.us/403');
112 | });
113 |
114 | domain.create('http').run(() => {
115 | TrackJS.configure({ userId: 'http' });
116 | const req = http.request('http://httpstat.us/503', { method: "GET" });
117 | req.end();
118 | });
119 |
120 | domain.create('https').run(() => {
121 | TrackJS.configure({ userId: 'https' });
122 | const req = https.request('https://httpstat.us/404', { method: "GET" });
123 | req.end();
124 | });
125 |
126 | domain.create('request').run(() => {
127 | TrackJS.configure({ userId: 'request' });
128 | request('https://httpstat.us/501');
129 | });
130 |
131 | domain.create('axios').run(() => {
132 | TrackJS.configure({ userId: 'axios' });
133 | axios.get('https://httpstat.us/401').catch(() => null);
134 | });
135 |
--------------------------------------------------------------------------------
/test/integration/require/index.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const { TrackJS } = require('../../../dist');
3 |
4 | console.log('Starting Require Test...');
5 |
6 | TrackJS.install({
7 | token: '8de4c78a3ec64020ab2ad15dea1ae9ff',
8 | console: { enabled: false },
9 | onError: function(payload) {
10 |
11 | if (!payload.environment.dependencies.express) {
12 | console.log("failed to discover dependencies", payload.environment.dependencies);
13 | process.exit(1);
14 | return false;
15 | }
16 |
17 | console.log('Require Tests PASSED');
18 | process.exit(0);
19 | return false;
20 | }
21 | });
22 |
23 | TrackJS.track("test");
24 |
--------------------------------------------------------------------------------
/test/integration/transmit/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const fs = require('fs')
3 | const https = require('https')
4 | const { TrackJS } = require('../../../dist');
5 |
6 | console.log('Starting Transmitter Test...');
7 |
8 | // Ignore our silly self-signed cert for now.
9 | process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0;
10 |
11 | const TESTS_EXPECTED = 2;
12 | let testsComplete = 0;
13 |
14 | function testComplete() {
15 | testsComplete++;
16 | if (testsComplete >= TESTS_EXPECTED) {
17 | console.log('Transmitter Tests PASSED');
18 | process.exit(0);
19 | }
20 | }
21 | function assertStrictEqual(thing1, thing2) {
22 | if (thing1 !== thing2) {
23 | console.error("Assertion strict equal failed", thing1, thing2, new Error().stack);
24 | process.exit(1);
25 | }
26 | }
27 |
28 | let fakeTrackJSServer = express()
29 | .use(express.json({ type: '*/*' }))
30 | .post('/capture', (req, res, next) => {
31 | assertStrictEqual(req.query.token, 'testtoken');
32 | assertStrictEqual(req.body.message, 'test');
33 |
34 | res.status(204);
35 | testComplete();
36 | })
37 | .get('/usage.gif', (req, res, next) => {
38 | assertStrictEqual(req.query.token, 'testtoken');
39 | assertStrictEqual(req.query.application, 'test-case');
40 |
41 | var buf = new Buffer(43);
42 | buf.write("R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", "base64");
43 | res.send(buf, { 'Content-Type': 'image/gif' }, 200);
44 | testComplete();
45 | });
46 |
47 | https.createServer({
48 | key: fs.readFileSync(__dirname + '/server.key'),
49 | cert: fs.readFileSync(__dirname + '/server.cert')
50 | }, fakeTrackJSServer).listen(3001);
51 |
52 |
53 | TrackJS.install({
54 | token: 'testtoken',
55 | application: 'test-case',
56 | captureURL: 'https://localhost:3001/capture',
57 | usageURL: 'https://localhost:3001/usage.gif'
58 | });
59 |
60 | TrackJS.usage();
61 | TrackJS.track('test');
62 |
--------------------------------------------------------------------------------
/test/integration/transmit/server.cert:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDSjCCAjICCQD6NihSrcUYKTANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJV
3 | UzELMAkGA1UECAwCTU4xEzARBgNVBAcMClN0aWxsd2F0ZXIxFDASBgNVBAoMC1RS
4 | QUNLSlMgTExDMSAwHgYJKoZIhvcNAQkBFhFoZWxsb0B0cmFja2pzLmNvbTAeFw0x
5 | OTA2MjAyMTM5MzRaFw0xOTA3MjAyMTM5MzRaMGcxCzAJBgNVBAYTAlVTMQswCQYD
6 | VQQIDAJNTjETMBEGA1UEBwwKU3RpbGx3YXRlcjEUMBIGA1UECgwLVFJBQ0tKUyBM
7 | TEMxIDAeBgkqhkiG9w0BCQEWEWhlbGxvQHRyYWNranMuY29tMIIBIjANBgkqhkiG
8 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxAtGUE23G+UdR50ZTCSbrL+VXnmjDcfl78lz
9 | mOsqSkCZBInJJ5v8/8wtEmoKeN/qMEYbyn7onRiW+9I4Jmd6FR7Dv7qMoVGJ/5U0
10 | gFOXzKp9+kF/LlBxZlMkUWOaPdQ/GCvEYm/2QwGgse6QIKoDB9Hmx3B+8aLmU4XD
11 | 0pKZAZBqDY8uyqNV/2G7m8RJXYSOMVFvgrqtkFtiU99bj0I1oDLxq+5bMW13NFbG
12 | pDKojePFLpLgtYfiqevBjO+8DE6R5f1ga/0f07f4uHpPCpsn7c/s4e1Ce34WQ/aK
13 | /nIC0lJgKJDqJAxOD5GKT3QNJp6Jao7m6Xc5UHsW610kxfj1IwIDAQABMA0GCSqG
14 | SIb3DQEBCwUAA4IBAQCFZeuxe9UOo+usv5MysGnDqu1DFpPJUdI7hg03zaouRbB7
15 | ikqr33CrvDRbOBcLv/6bfWN2wWs2VTi/umh9idUZTRy4qRXY9kjMRQ9v6UdcPtR8
16 | biHzw9QeWKF5xeAs9Ev32CEaLvKWNaVvPwFzncTUWpDqxNe7RxfcUmdumHrRYSRZ
17 | Pc0FsGTMmer6nVjNIv/xIvdtI5eirCn4004g233NMrihpCgYQDFNQHVQN55oSM7h
18 | nXX+ogOb8ys8nJwmznwQExCkCjrsN5H+qn/zdAX8Tgzs4y8635jVmXxzGiNYk0Ax
19 | brmcxa9qL0LxbHtGaJ27XkKh1SrVEfzSQ/5pI2Iq
20 | -----END CERTIFICATE-----
21 |
--------------------------------------------------------------------------------
/test/integration/transmit/server.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDEC0ZQTbcb5R1H
3 | nRlMJJusv5VeeaMNx+XvyXOY6ypKQJkEicknm/z/zC0Sagp43+owRhvKfuidGJb7
4 | 0jgmZ3oVHsO/uoyhUYn/lTSAU5fMqn36QX8uUHFmUyRRY5o91D8YK8Rib/ZDAaCx
5 | 7pAgqgMH0ebHcH7xouZThcPSkpkBkGoNjy7Ko1X/YbubxEldhI4xUW+Cuq2QW2JT
6 | 31uPQjWgMvGr7lsxbXc0VsakMqiN48UukuC1h+Kp68GM77wMTpHl/WBr/R/Tt/i4
7 | ek8Kmyftz+zh7UJ7fhZD9or+cgLSUmAokOokDE4PkYpPdA0mnolqjubpdzlQexbr
8 | XSTF+PUjAgMBAAECggEAGek7k+WRrBguNIbpDw1Pnhd8UI5XJL0lAPppUu7SGhpL
9 | nzPu6FNPryd3VZW6aJt/wn2hsrPvXsQfQI1aaokGs7/rM24epDu4t5Uq6UMjLyPh
10 | nrAFdUOgmVun0zIM8tsYs8MlI7WUoOqYKKxVVFvkJqZxkC6oOJvFQYjOhwhlPLV0
11 | djeuuqSHPleEOgvObYr9bx0XDTnmJLNrdOTD/Brg4dnM/EweIzNkTmMArSAXE5+H
12 | PAxzc4H8hfX9ABUgz7sqySOX4jZe4sX03TQsPzBhHlcsWVj5xq6Krk+fxldGpm6E
13 | GGAH7y+S3xA2s8l0v6XB2dRy5fY2xE4tWknnTo4hQQKBgQDh5jp/OH++Sm+kdThy
14 | W4h/i0UPoSWOexsbtSvxZv7k95ioSzUC3fkP8kzPgUBSTzN9D9MQHdsJSBfFAqpM
15 | qReb7DxJexs7n3PB6OZXqj5wzbwqJPETwKAXXCUt1VkcDtGrW+yFIpJWGqYnLQrN
16 | 4vwiJTfJdNc1aSrBLpSb73qXcQKBgQDeKqL6GO3Ol9d5hTzMJqYz0zEiopqE2b3R
17 | dQXJS/BqWW+V3JVVvs9i4/VOFOpKyUfezQLb4is5BYtQPGvUUeV+Y7mcl3F+RFGz
18 | nT7s0JdXlZOm0tDw2mv+aE+tQQFYywjCTxdpOcMz38kccLm8x1BQ8KDrnWCR7TJg
19 | e0DzdNzT0wKBgQCy4VTBGUROszLkz837AHB+TxJI5hqPUPyOXFCHwD9noIjJ+4Pm
20 | 2U84SW55gqgkhYMILVe9E2c3Wlc4MnwYaG3BkrfKmClgdUt86ag9ExqOyNuPo+Ei
21 | +w3KcbUfLip6BWISh6ArbMzkUgXwT8KWdn+hqkcax+MC8FxizTfwoxo4gQKBgBSK
22 | JpUuttWxzhcLDWrma9lAvjswg06i/tydkBJjNe/vItTdFuVdhbjj0GsOnfaaE03y
23 | 8D+58jsMWyvo2iNACxKbT669hZ2h3VfYwteMFWHgj00OEokh+HgF1s/ywZge9UlZ
24 | nJV65KQoHWTGuzVLzA4foREHdMUpA85NttyLQltHAoGAHRqHrCGYuCnXLETCElCP
25 | OOu/cvgIBXNtXtz3BtiUtXfi2YzR2GuNs7Gq1WwbY8UAgQDHwcE/7jcDTQ1cm/0E
26 | Cz6FP7uZzqsN+Ze/OxtQhpfPLdWnuLvSx9PuuvFSCYAr4VQitMUYisvQNux7FGVF
27 | oodUtiouGQ701zmfJ4deNXQ=
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/test/sdk.test.ts:
--------------------------------------------------------------------------------
1 | import * as TrackJS from "../src/sdk";
2 | import { TrackJSError } from "../src/types/TrackJSError";
3 | import { AgentRegistrar } from "../src/AgentRegistrar";
4 |
5 | describe("sdk", () => {
6 | describe("error checking", () => {
7 | it("install throws without options", () => {
8 | expect(() => TrackJS.install(null)).toThrow(TrackJSError);
9 | });
10 | it("install throws without token", () => {
11 | expect(() => TrackJS.install({ token: "" })).toThrow(TrackJSError);
12 | });
13 | it("install throws if already installed", () => {
14 | expect(() => TrackJS.install({ token: "" })).toThrow(TrackJSError);
15 | });
16 | it("uninstall returns okay without install", () => {
17 | expect(() => TrackJS.uninstall()).not.toThrow();
18 | });
19 | it("addMetadata throws when not installed", () => {
20 | expect(() => TrackJS.addMetadata("foo", "bar")).toThrow(TrackJSError);
21 | });
22 | it("removeMetadata throws when not installed", () => {
23 | expect(() => TrackJS.addMetadata("foo", "bar")).toThrow(TrackJSError);
24 | });
25 | it("addLogTelemetry throws when not installed", () => {
26 | expect(() => TrackJS.addLogTelemetry("log", "test")).toThrow(TrackJSError);
27 | });
28 | it("onError throws when not installed", () => {
29 | expect(() => TrackJS.onError((payload) => false)).toThrow(TrackJSError);
30 | });
31 | it("Handlers.expressErrorHandler throws when not installed", () => {
32 | expect(() => TrackJS.Handlers.expressErrorHandler()).toThrow(TrackJSError);
33 | });
34 | });
35 |
36 | describe("console", () => {
37 | beforeEach(() => {
38 | TrackJS.install({ token: "test" });
39 | });
40 | afterEach(() => {
41 | TrackJS.uninstall();
42 | });
43 |
44 | it("can capture a log message to telemetry", () => {
45 | TrackJS.console.log("a log message", 42, {});
46 | expect(AgentRegistrar.getCurrentAgent().telemetry.getAllByCategory("c")).toEqual([
47 | expect.objectContaining({
48 | message: '["a log message",42,{}]',
49 | severity: "log",
50 | timestamp: expect.any(String)
51 | })
52 | ]);
53 | });
54 |
55 | it("can capture a log message to telemetry with different severity", () => {
56 | TrackJS.console.info("a info message");
57 | TrackJS.console.warn("a warn message");
58 | expect(AgentRegistrar.getCurrentAgent().telemetry.getAllByCategory("c")).toEqual([
59 | expect.objectContaining({
60 | message: "a info message",
61 | severity: "info",
62 | timestamp: expect.any(String)
63 | }),
64 | expect.objectContaining({
65 | message: "a warn message",
66 | severity: "warn",
67 | timestamp: expect.any(String)
68 | })
69 | ]);
70 | });
71 |
72 | it("can capture a console error message", () => {
73 | let errorTracked;
74 | AgentRegistrar.getCurrentAgent().captureError = jest.fn((error) => {
75 | errorTracked = error;
76 | return true;
77 | });
78 |
79 | TrackJS.console.error("a console error", 42, {});
80 | expect(AgentRegistrar.getCurrentAgent().telemetry.getAllByCategory("c")).toEqual([]);
81 | expect(errorTracked.message).toEqual('["a console error",42,{}]');
82 | expect(errorTracked.stack).toBeDefined();
83 | });
84 | });
85 |
86 | describe("track()", () => {
87 | it("serializes non error data", () => {
88 | let errorTracked: Error;
89 | TrackJS.install({ token: "test" });
90 | AgentRegistrar.getCurrentAgent().captureError = jest.fn((error) => {
91 | errorTracked = error;
92 | return true;
93 | });
94 | TrackJS.track("a string");
95 | expect(errorTracked.message).toEqual("a string");
96 | TrackJS.track(42);
97 | expect(errorTracked.message).toEqual("42");
98 | TrackJS.track({ foo: "bar" });
99 | expect(errorTracked.message).toEqual('{"foo":"bar"}');
100 | });
101 | });
102 | });
103 |
--------------------------------------------------------------------------------
/test/telemetry/ConsoleTelemetry.test.ts:
--------------------------------------------------------------------------------
1 | import { ConsoleTelemetry } from "../../src/telemetry";
2 |
3 | describe("ConsoleTelemetryData", () => {
4 | describe("normalizeSeverity()", () => {
5 | test("it returns supported values", () => {
6 | expect.assertions(5);
7 | ["debug", "info", "warn", "error", "log"].forEach((sev) => {
8 | expect(ConsoleTelemetry.normalizeSeverity(sev)).toBe(sev);
9 | });
10 | });
11 |
12 | test("it normalizes casing", () => {
13 | expect(ConsoleTelemetry.normalizeSeverity("DEBUG")).toBe("debug");
14 | expect(ConsoleTelemetry.normalizeSeverity("eRrOr")).toBe("error");
15 | expect(ConsoleTelemetry.normalizeSeverity("loG")).toBe("log");
16 | });
17 |
18 | test("it returns default for unsupported", () => {
19 | expect.assertions(5);
20 | ["", "custom", "a really really really long value", "something else", "false"].forEach((sev) => {
21 | expect(ConsoleTelemetry.normalizeSeverity(sev)).toBe("log");
22 | });
23 | });
24 | });
25 |
26 | describe("constructor()", () => {
27 | test("it handles weird message formats", () => {
28 | expect(new ConsoleTelemetry("log", ["a message"]).message).toBe("a message");
29 | expect(new ConsoleTelemetry("log", [1, 2, 3, 4, 5]).message).toBe("[1,2,3,4,5]");
30 | expect(new ConsoleTelemetry("log", [{ foo: "bar" }]).message).toBe('{"foo":"bar"}');
31 | expect(new ConsoleTelemetry("log", [{ foo: "bar" }, { bar: "baz" }]).message).toBe(
32 | '[{"foo":"bar"},{"bar":"baz"}]'
33 | );
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/telemetry/TelemetryBuffer.test.ts:
--------------------------------------------------------------------------------
1 | import { TelemetryBuffer } from "../../src/telemetry";
2 |
3 | describe("TelemetryBuffer", () => {
4 | let telemetry: TelemetryBuffer;
5 | let bufferSize = 5;
6 |
7 | beforeEach(() => {
8 | telemetry = new TelemetryBuffer(bufferSize);
9 | });
10 |
11 | describe("add()", () => {
12 | it("adds telemetry to buffer", () => {
13 | telemetry.add("test", {});
14 | expect(telemetry.count()).toBe(1);
15 | });
16 |
17 | it("rolls old items from the buffer", () => {
18 | for (var i = 0; i < 40; i++) {
19 | telemetry.add("test", {});
20 | }
21 | expect(telemetry.count()).toBe(bufferSize);
22 | });
23 | });
24 |
25 | describe("clear()", () => {
26 | it("it empties the buffer", () => {
27 | let key = telemetry.add("test", {});
28 | telemetry.clear();
29 | expect(telemetry.count()).toBe(0);
30 | expect(telemetry.get(key)).toBeNull();
31 | });
32 | });
33 |
34 | describe("clone()", () => {
35 | it("it creates equal buffer", () => {
36 | telemetry.add("test", {});
37 | telemetry.add("test", {});
38 | var buffer2 = telemetry.clone();
39 | expect(telemetry.getAllByCategory("test")).toEqual(buffer2.getAllByCategory("test"));
40 | });
41 | it("it can be cross-referenced", () => {
42 | let key1 = telemetry.add("test", {});
43 | var buffer2 = telemetry.clone();
44 | expect(telemetry.get(key1)).toBe(buffer2.get(key1));
45 | });
46 | it("has separate telemetry", () => {
47 | telemetry.add("t", { foo: "bar" });
48 | let telemetry2 = telemetry.clone();
49 | telemetry2.add("t", { bar: "baz" });
50 | expect(telemetry.getAllByCategory("t")).not.toEqual(telemetry2.getAllByCategory("t"));
51 | });
52 | });
53 |
54 | describe("count()", () => {
55 | it("it returns correct count", () => {
56 | telemetry.add("test", {});
57 | telemetry.add("test", {});
58 | telemetry.add("test", {});
59 | expect(telemetry.count()).toBe(3);
60 | });
61 | });
62 |
63 | describe("get()", () => {
64 | it("returns items from store", () => {
65 | let item = { foo: "bar" };
66 | let key = telemetry.add("test", item);
67 | expect(telemetry.get(key)).toBe(item);
68 | });
69 | });
70 |
71 | describe("getAllByCategory()", () => {
72 | it("returns all objects from category", () => {
73 | let telemetry1 = {};
74 | let telemetry2 = {};
75 | let telemetry3 = {};
76 | let telemetry4 = {};
77 | let telemetry5 = {};
78 | telemetry.add("test", telemetry1);
79 | telemetry.add("other", telemetry2);
80 | telemetry.add("other", telemetry3);
81 | telemetry.add("test", telemetry4);
82 | telemetry.add("test", telemetry5);
83 |
84 | expect(telemetry.getAllByCategory("test")).toEqual([telemetry1, telemetry4, telemetry5]);
85 | expect(telemetry.getAllByCategory("other")).toEqual([telemetry2, telemetry3]);
86 | expect(telemetry.getAllByCategory("empty")).toEqual([]);
87 | });
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/test/utils/cli.test.ts:
--------------------------------------------------------------------------------
1 | import { cli } from "../../src/utils/cli";
2 |
3 | const CURRENT_VERSION = require("../../package.json").version;
4 |
5 | describe("cli()", () => {
6 | it("executes commands and returns results", () => {
7 | expect.assertions(1);
8 | return cli("npm version --json --parseable").then((data) => {
9 | let result = JSON.parse(data.toString());
10 | expect(result["trackjs-node"]).toBe(CURRENT_VERSION);
11 | });
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/test/utils/isType.test.ts:
--------------------------------------------------------------------------------
1 | import * as isType from "../../src/utils/isType";
2 |
3 | describe("isType", () => {
4 | describe("isArray(thing)", () => {
5 | test("returns true for Array literal", () => {
6 | expect(isType.isArray([])).toBe(true);
7 | });
8 | test("returns true for Array instance", () => {
9 | expect(isType.isArray(new Array())).toBe(true); // eslint-disable-line
10 | expect(isType.isArray(Array())).toBe(true); // eslint-disable-line
11 | });
12 | test("returns false for other things", () => {
13 | expect(isType.isArray(new Object())).toBe(false); // eslint-disable-line
14 | expect(isType.isArray({})).toBe(false);
15 | expect(isType.isArray("[object Array]")).toBe(false);
16 | expect(isType.isArray(0)).toBe(false);
17 | expect(isType.isArray(null)).toBe(false);
18 | expect(isType.isArray(undefined)).toBe(false);
19 | expect(isType.isArray(NaN)).toBe(false);
20 | });
21 | });
22 |
23 | describe("isBoolean(thing)", () => {
24 | test("returns true for a Boolean literal", () => {
25 | expect(isType.isBoolean(true)).toBe(true);
26 | expect(isType.isBoolean(false)).toBe(true);
27 | });
28 | test("returns true for a Boolean instance", () => {
29 | expect(isType.isBoolean(new Boolean("true"))).toBe(true); // eslint-disable-line
30 | expect(isType.isBoolean(new Boolean())).toBe(true); // eslint-disable-line
31 | expect(isType.isBoolean(Boolean(0))).toBe(true);
32 | });
33 | test("returns false for other things", () => {
34 | expect(isType.isBoolean("false")).toBe(false);
35 | expect(isType.isBoolean(0)).toBe(false);
36 | expect(isType.isBoolean(new Object())).toBe(false); // eslint-disable-line
37 | expect(isType.isBoolean({})).toBe(false);
38 | expect(isType.isBoolean([0])).toBe(false);
39 | expect(isType.isBoolean(null)).toBe(false);
40 | expect(isType.isBoolean(undefined)).toBe(false);
41 | });
42 | });
43 |
44 | describe("isError(thing)", () => {
45 | test("returns true when error instance", () => {
46 | expect(isType.isError(new Error("test"))).toBe(true);
47 | });
48 | test("returns true when caught error", () => {
49 | var e;
50 | try {
51 | expect["undef"]();
52 | } catch (err) {
53 | e = err;
54 | }
55 | expect(isType.isError(e)).toBe(true);
56 | });
57 | test("returns true when DOM Exception", () => {
58 | expect(isType.isError(new DOMException("test"))).toBe(true);
59 | });
60 | test("returns true for error-shaped objects", () => {
61 | expect(isType.isError({ name: "TestError", message: "message" })).toBe(true);
62 | });
63 | test("returns false for other things", () => {
64 | expect(isType.isError(new Object())).toBe(false); // eslint-disable-line
65 | expect(isType.isError({})).toBe(false);
66 | expect(isType.isError("[object Array]")).toBe(false);
67 | expect(isType.isError(0)).toBe(false);
68 | expect(isType.isError(null)).toBe(false);
69 | expect(isType.isError(undefined)).toBe(false);
70 | expect(isType.isError(NaN)).toBe(false);
71 | });
72 | });
73 |
74 | describe("isFunction()", function() {
75 | test("returns true with function", function() {
76 | expect(isType.isFunction(function() {})).toBe(true);
77 | });
78 |
79 | test("returns false with non-function", function() {
80 | expect(isType.isFunction(null)).toBe(false);
81 | expect(isType.isFunction(undefined)).toBe(false);
82 | expect(isType.isFunction(NaN)).toBe(false);
83 | expect(isType.isFunction(42)).toBe(false);
84 | expect(isType.isFunction("string")).toBe(false);
85 | expect(isType.isFunction(true)).toBe(false);
86 | expect(isType.isFunction({})).toBe(false);
87 | expect(isType.isFunction([])).toBe(false);
88 | });
89 | });
90 |
91 | describe("isNumber(thing)", function() {
92 | test("returns true for a Number literal", () => {
93 | expect(isType.isNumber(42)).toBe(true);
94 | expect(isType.isNumber(0)).toBe(true);
95 | expect(isType.isNumber(NaN)).toBe(true);
96 | });
97 | test("returns true for a Number instance", () => {
98 | expect(isType.isNumber(new Number(42.2123))).toBe(true); // eslint-disable-line
99 | expect(isType.isNumber(new Number())).toBe(true); // eslint-disable-line
100 | expect(isType.isNumber(Number(0))).toBe(true);
101 | });
102 | test("returns false for other things", () => {
103 | expect(isType.isNumber(new Object())).toBe(false); // eslint-disable-line
104 | expect(isType.isNumber({})).toBe(false);
105 | expect(isType.isNumber([0])).toBe(false);
106 | expect(isType.isNumber(null)).toBe(false);
107 | expect(isType.isNumber(undefined)).toBe(false);
108 | });
109 | });
110 |
111 | describe("isObject()", function() {
112 | test("returns true with object", function() {
113 | expect(isType.isObject({})).toBe(true);
114 | });
115 |
116 | test("returns true with arrays", function() {
117 | expect(isType.isObject([])).toBe(true);
118 | });
119 |
120 | test("returns false with non-object", function() {
121 | expect(isType.isObject(null)).toBe(false);
122 | expect(isType.isObject(undefined)).toBe(false);
123 | expect(isType.isObject(NaN)).toBe(false);
124 | expect(isType.isObject(42)).toBe(false);
125 | expect(isType.isObject("string")).toBe(false);
126 | expect(isType.isObject(true)).toBe(false);
127 | expect(isType.isObject(function() {})).toBe(false);
128 | });
129 | });
130 |
131 | describe("isString(thing)", () => {
132 | test("returns true for a String literal", () => {
133 | expect(isType.isString("hey")).toBe(true);
134 | expect(isType.isString("")).toBe(true);
135 | });
136 | test("returns true for a String instance", () => {
137 | expect(isType.isString(new String("hey"))).toBe(true); // eslint-disable-line
138 | expect(isType.isString(new String(""))).toBe(true); // eslint-disable-line
139 | expect(isType.isString(String(""))).toBe(true);
140 | });
141 | test("returns false for other things", () => {
142 | expect(isType.isString(new Object())).toBe(false); // eslint-disable-line
143 | expect(isType.isString({})).toBe(false);
144 | expect(isType.isString(["string"])).toBe(false);
145 | expect(isType.isString(0)).toBe(false);
146 | expect(isType.isString(null)).toBe(false);
147 | expect(isType.isString(undefined)).toBe(false);
148 | expect(isType.isString(NaN)).toBe(false);
149 | });
150 | });
151 | });
152 |
--------------------------------------------------------------------------------
/test/utils/nestedAssign.test.ts:
--------------------------------------------------------------------------------
1 | import { nestedAssign } from "../../src/utils/nestedAssign";
2 |
3 | describe("nestedAssign()", () => {
4 | it("assigns properties from objects", () => {
5 | const a = { foo: "foo" };
6 | const b = { bar: "bar" };
7 | const c = { foo: "baz" };
8 | const result = nestedAssign({}, a, b, c);
9 | expect(result).not.toBe(a);
10 | expect(result).not.toBe(b);
11 | expect(result).not.toBe(c);
12 | expect(result).toEqual({
13 | foo: "baz",
14 | bar: "bar"
15 | });
16 | });
17 |
18 | it("assigns nested properties from objects when always provided", () => {
19 | const a = { foo: "foo", nested: { bar: "bar", baz: "baz" } };
20 | const b = { foo: "zzz", nested: { bar: "xxx", baz: "yyy" } };
21 | const result = nestedAssign({}, a, b);
22 | expect(result).toEqual({
23 | foo: "zzz",
24 | nested: {
25 | bar: "xxx",
26 | baz: "yyy"
27 | }
28 | });
29 | });
30 |
31 | it("assigns nested properties from objects when not provided", () => {
32 | const a = { foo: "foo", nested: { bar: "bar", baz: "baz" } };
33 | const b = { foo: "zzz" };
34 | const result = nestedAssign({}, a, b);
35 | expect(result).toEqual({
36 | foo: "zzz",
37 | nested: {
38 | bar: "bar",
39 | baz: "baz"
40 | }
41 | });
42 | });
43 |
44 | it("assigns nested properties from objects when partial provided", () => {
45 | const a = { foo: "foo", nested: { bar: "bar", baz: "baz" } };
46 | const b = { foo: "zzz", nested: { baz: "xxx" } };
47 | const result = nestedAssign({}, a, b);
48 | expect(result).toEqual({
49 | foo: "zzz",
50 | nested: {
51 | bar: "bar",
52 | baz: "xxx"
53 | }
54 | });
55 | });
56 |
57 | it("doesn't manipulate the sources", () => {
58 | const a = { foo: "foo", nested: { bar: "bar", baz: "baz" } };
59 | const b = { foo: "zzz", nested: { baz: "xxx" } };
60 | const result = nestedAssign({}, a, b);
61 | expect(a).toEqual({ foo: "foo", nested: { bar: "bar", baz: "baz" } });
62 | expect(b).toEqual({ foo: "zzz", nested: { baz: "xxx" } });
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/test/utils/patch.test.ts:
--------------------------------------------------------------------------------
1 | import { patch, unpatch } from "../../src/utils/patch";
2 |
3 | describe("patch(obj, name, func)", () => {
4 | test("replaces original function", () => {
5 | var func = jest.fn();
6 | var obj = { func };
7 | patch(obj, "func", function() {
8 | return function() {};
9 | });
10 | expect(obj.func).not.toBe(func);
11 | });
12 |
13 | test("creates new function", () => {
14 | var obj = {};
15 | patch(obj, "func2", function() {
16 | return function() {};
17 | });
18 | expect(obj["func2"]).toBeDefined();
19 | });
20 |
21 | test("calls through with original function", () => {
22 | var func = jest.fn();
23 | var obj = { func };
24 | patch(obj, "func", function(original) {
25 | return function() {
26 | original.apply(this, arguments);
27 | };
28 | });
29 | obj.func("a", "b");
30 | expect(func).toHaveBeenCalledWith("a", "b");
31 | });
32 | });
33 |
34 | describe("unpatch(obj, name)", () => {
35 | test("restores a patched function to original", () => {
36 | var func = jest.fn();
37 | var obj = { func };
38 | patch(
39 | obj,
40 | "func",
41 | (original) =>
42 | function() {
43 | return original();
44 | }
45 | );
46 | expect(obj.func).not.toBe(func);
47 | unpatch(obj, "func");
48 | expect(obj.func).toBe(func);
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/test/utils/serialize.test.ts:
--------------------------------------------------------------------------------
1 | import { serialize } from "../../src/utils/serialize";
2 |
3 | describe("serialize()", () => {
4 | test("serializes strings", () => {
5 | expect(serialize("string")).toBe("string");
6 | expect(serialize("")).toBe("Empty String");
7 | expect(serialize(new String("a string"))).toBe("a string"); // eslint-disable-line
8 | });
9 | test("serializes numbers", () => {
10 | expect(serialize(42)).toBe("42");
11 | expect(serialize(0)).toBe("0");
12 | expect(serialize(NaN)).toBe("NaN");
13 | expect(serialize(new Number("32.12"))).toBe("32.12"); // eslint-disable-line
14 | });
15 | test("serializes booleans", () => {
16 | expect(serialize(true)).toBe("true");
17 | expect(serialize(false)).toBe("false");
18 | expect(serialize(new Boolean("true"))).toBe("true"); // eslint-disable-line
19 | expect(serialize(new Boolean())).toBe("false"); // eslint-disable-line
20 | });
21 | test("serializes falsy types", () => {
22 | expect(serialize(undefined)).toBe("undefined");
23 | expect(serialize(null)).toBe("null");
24 | expect(serialize("")).toBe("Empty String");
25 | expect(serialize(0)).toBe("0");
26 | expect(serialize(false)).toBe("false");
27 | expect(serialize(NaN)).toBe("NaN");
28 | });
29 | test("serializes objects", () => {
30 | expect(serialize({})).toBe("{}");
31 | expect(serialize({ foo: "bar" })).toBe('{"foo":"bar"}');
32 | expect(serialize(new Object())) // eslint-disable-line
33 | .toBe("{}");
34 | });
35 | test("serializes functions", () => {
36 | expect(serialize(function() {})).toBe("function () { }");
37 | // prettier-ignore
38 | expect(serialize(function xxx(foo, bar) { return foo + bar; }))
39 | .toBe("function xxx(foo, bar) { return foo + bar; }");
40 | });
41 | test("serializes arrays", () => {
42 | expect(serialize(["a string", 42, false])).toBe('["a string",42,false]');
43 | expect(serialize([null, NaN, 0, "", undefined])).toBe('[null,NaN,0,"",undefined]');
44 | expect(serialize([{ foo: "bar" }])).toBe('[{"foo":"bar"}]');
45 | expect(serialize([[]])).toBe("[[]]");
46 | expect(serialize([new Error("test")])).toContain('[{"name":"Error","message":"test","stack":"Error: test');
47 | });
48 | test("fallsback to simple serialization for circular references", () => {
49 | var foo = {
50 | a: "1",
51 | b: "2"
52 | };
53 | foo["bar"] = foo;
54 | expect(serialize(foo)).toBe('{"a":"1","b":"2","bar":"[object Object]"}');
55 | });
56 | test("serializes Errors", () => {
57 | var e;
58 | try {
59 | throw new Error("oh crap");
60 | } catch (err) {
61 | e = err;
62 | }
63 | // NOTE [Todd Gardner] The actual stack trace is somewhat unpredictable
64 | // inside of Karma/Jasmine, so we just match on the starting characters
65 | // for test stability.
66 | expect(serialize(e)).toContain('{"name":"Error","message":"oh crap","stack":"Error: oh crap');
67 |
68 | expect(serialize(new Error("test"))).toContain('{"name":"Error","message":"test","stack":"Error: test');
69 | });
70 |
71 | test("serializes symbols", () => {
72 | expect(serialize(Symbol())).toBe("Symbol()");
73 | expect(serialize(Symbol("name"))).toBe("Symbol(name)");
74 | expect(serialize(Symbol(42))).toBe("Symbol(42)");
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/test/utils/truncateString.test.ts:
--------------------------------------------------------------------------------
1 | import { truncateString } from "../../src/utils/truncateString";
2 |
3 | describe("truncateString()", () => {
4 | test("returns strings shorter then length unchanged", () => {
5 | expect(truncateString("a normal string", 15)).toBe("a normal string");
6 | });
7 |
8 | test("appends ellipsis and length to long string", () => {
9 | expect(truncateString("a too long string", 5)).toBe("a too...{12}");
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/test/utils/uuid.test.ts:
--------------------------------------------------------------------------------
1 | import { uuid } from "../../src/utils/uuid";
2 |
3 | describe("uuid()", () => {
4 | it("generates a uuid string", () => {
5 | expect(uuid()).toMatch(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/);
6 | });
7 | // it('generates unique uuids', () => {
8 | // let set = new Set();
9 | // for(var idx = 0; idx <= 1000; idx++) {
10 | // let id = uuid();
11 | // expect(set.has(id)).toBe(false);
12 | // set.add(id);
13 | // }
14 | // });
15 | });
16 |
--------------------------------------------------------------------------------
/test/watchers/ConsoleWatcher.test.ts:
--------------------------------------------------------------------------------
1 | import { ConsoleWatcher } from "../../src/watchers";
2 | import { ConsoleTelemetry } from "../../src/telemetry";
3 | import { Agent } from "../../src/Agent";
4 | import { AgentRegistrar } from "../../src/AgentRegistrar";
5 |
6 | let _ConsoleWatcher = ConsoleWatcher as any;
7 |
8 | beforeAll(() => {
9 | Agent.defaults.dependencies = false;
10 | });
11 |
12 | describe("ConsoleWatcher", () => {
13 | describe("install()", () => {
14 | let fakeOptions = null;
15 | let fakeConsole = null;
16 | let fakeAgent = null;
17 |
18 | beforeEach(() => {
19 | fakeOptions = {};
20 | fakeConsole = jest.mock("console");
21 | fakeAgent = new Agent({ token: "test" });
22 | AgentRegistrar.init(fakeAgent);
23 | });
24 |
25 | afterEach(() => {
26 | AgentRegistrar.close();
27 | });
28 |
29 | it("console patches add telemetry", () => {
30 | fakeAgent.telemetry.add = jest.fn();
31 | _ConsoleWatcher.install(fakeOptions, fakeConsole);
32 | fakeConsole.log("a log message");
33 | expect(fakeAgent.telemetry.add).toHaveBeenCalledWith("c", expect.any(ConsoleTelemetry));
34 | });
35 |
36 | it("calls through to console", () => {
37 | let consoleLogSpy = jest.spyOn(fakeConsole, "log");
38 | _ConsoleWatcher.install(fakeOptions, fakeConsole);
39 | fakeConsole.log("a log message", 2, 3, 4);
40 | expect(consoleLogSpy).toHaveBeenCalledWith("a log message", 2, 3, 4);
41 | });
42 |
43 | it("captures on console error", () => {
44 | fakeAgent.captureError = jest.fn((error) => null);
45 | _ConsoleWatcher.install(fakeOptions, fakeConsole);
46 | fakeConsole.error("oops");
47 | expect(fakeAgent.captureError).toHaveBeenCalled();
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/test/watchers/NetworkWatcher.test.ts:
--------------------------------------------------------------------------------
1 | import http from "http";
2 | import https from "https";
3 | import { NetworkWatcher, Watcher } from "../../src/watchers";
4 | import { Agent } from "../../src/Agent";
5 | import { AgentRegistrar } from "../../src/AgentRegistrar";
6 | import { TrackJSOptions } from "../../src/types";
7 | import { TrackJSEntry } from "../../src/types/TrackJSCapturePayload";
8 |
9 | const _NetworkWatcher = NetworkWatcher as Watcher;
10 |
11 | beforeAll(() => {
12 | Agent.defaults.dependencies = false;
13 | });
14 |
15 | jest.mock("net");
16 |
17 | describe("NetworkWatcher", () => {
18 | describe("install()", () => {
19 | let fakeOptions: TrackJSOptions;
20 | let fakeAgent: Agent;
21 |
22 | beforeEach(() => {
23 | fakeOptions = { network: { error: true, enabled: true } };
24 | fakeAgent = new Agent({ token: "test" });
25 | AgentRegistrar.init(fakeAgent);
26 | });
27 |
28 | afterEach(() => {
29 | _NetworkWatcher.uninstall();
30 | });
31 |
32 | it("when disabled, does nothing", (done) => {
33 | _NetworkWatcher.install({ network: { enabled: false } });
34 | fakeAgent.telemetry.add = jest.fn();
35 | http.get("http://example.com/?foo=bar", () => {
36 | expect(fakeAgent.telemetry.add).not.toHaveBeenCalled();
37 | done();
38 | });
39 | });
40 |
41 | it("when enabled, http patched to intercept request", (done) => {
42 | _NetworkWatcher.install({ network: { enabled: true } });
43 | fakeAgent.telemetry.add = jest.fn();
44 |
45 | const req = http.request("http://example.com/?foo=bar", { method: "GET" }, (resp) => {
46 | expect(fakeAgent.telemetry.add).toHaveBeenCalledWith(
47 | "n",
48 | expect.objectContaining({
49 | method: "GET",
50 | url: "http://example.com/?foo=bar",
51 | startedOn: expect.any(String)
52 | })
53 | );
54 | done();
55 | });
56 | req.end();
57 | });
58 |
59 | it("when enabled, https patched to intercept request", (done) => {
60 | _NetworkWatcher.install(fakeOptions);
61 | fakeAgent.telemetry.add = jest.fn();
62 | const req = https.request("https://example.com/?foo=bar", { method: "GET" }, (res) => {
63 | expect(fakeAgent.telemetry.add).toHaveBeenCalledWith(
64 | "n",
65 | expect.objectContaining({
66 | method: "GET",
67 | url: "https://example.com/?foo=bar",
68 | startedOn: expect.any(String)
69 | })
70 | );
71 | done();
72 | });
73 | req.end();
74 | });
75 |
76 | it("when error enabled, captures error from failing request", (done) => {
77 | _NetworkWatcher.install({ network: { enabled: true, error: true } });
78 | fakeAgent.telemetry.add = jest.fn();
79 | fakeAgent.captureError = jest.fn();
80 | const req = https.request("https://httpstat.us/501", { method: "GET" }, (res) => {
81 | setTimeout(() => {
82 | expect(fakeAgent.captureError).toHaveBeenCalledWith(
83 | expect.objectContaining({
84 | stack: expect.any(String)
85 | }),
86 | TrackJSEntry.Network
87 | );
88 | done();
89 | });
90 | });
91 | req.end();
92 | });
93 |
94 | it("when error disabled, no errors captured", (done) => {
95 | _NetworkWatcher.install({ network: { enabled: true, error: false } });
96 | fakeAgent.telemetry.add = jest.fn();
97 | fakeAgent.captureError = jest.fn();
98 | const req = https.request("https://httpstat.us/501", { method: "GET" }, (res) => {
99 | setTimeout(() => {
100 | expect(fakeAgent.captureError).not.toHaveBeenCalled();
101 | done();
102 | });
103 | });
104 | req.end();
105 | });
106 | });
107 | });
108 |
--------------------------------------------------------------------------------
/tools/release-canary.ps1:
--------------------------------------------------------------------------------
1 | #requires -version 4
2 | <#
3 | .SYNOPSIS Releases the agent to canary. This is invoked by TeamCity after
4 | the build and tests have been completed.
5 | .PARAMETER $Root Root directory of the project.
6 | .PARAMETER $BuildNumber Current Build Count Number.
7 | #>
8 |
9 | Param (
10 | [string] $Root = $pwd,
11 | [Parameter(Mandatory)][string] $BuildNumber
12 | )
13 |
14 |
15 | # Check if the environment is compatible with the script
16 | #############################################################################
17 | if (-Not (Get-ChildItem -Path $Root -Name package.json)) {
18 | Write-Error "package.json configuration missing from $Path"
19 | exit 1
20 | }
21 | if (-Not (Get-Command npm)) {
22 | Write-Error "Environment missing required command: npm"
23 | exit 1
24 | }
25 |
26 |
27 | try {
28 | Write-Output "TrackJS-Node Canary Release Starting..."
29 |
30 | # Update package.json with build information for release
31 | #############################################################################
32 | $Package = Get-Content "$Root/package.json" -raw | ConvertFrom-Json
33 | $Package.version = $PackageVersion = $Package.version + "-canary.$BuildNumber"
34 | Write-Output "Updating package version to $PackageVersion"
35 | Write-Output "##teamcity[buildNumber '$PackageVersion']"
36 | $Package | ConvertTo-Json | Set-Content "$Root/package.json"
37 |
38 | # Publish to npm
39 | #############################################################################
40 | Write-Output "Publishing to npm"
41 | & npm publish $Root --tag canary
42 | if ($lastExitCode -ne 0) {
43 | Write-Error "Failed to publish to npm"
44 | exit 1;
45 | }
46 |
47 | Write-Output "TrackJS-Node Canary Release Complete."
48 | } catch {
49 | Write-Error $_.Exception.Message
50 | exit 1
51 | }
52 |
--------------------------------------------------------------------------------
/tools/release-production.ps1:
--------------------------------------------------------------------------------
1 | #requires -version 4
2 | <#
3 | .SYNOPSIS Releases the agent to canary. This is invoked by TeamCity after
4 | the build and tests have been completed.
5 | .PARAMETER $Root Root directory of the project.
6 | .PARAMETER $BuildNumber Current Build Count Number.
7 | #>
8 |
9 | Param (
10 | [string] $Root = $pwd,
11 | [Parameter(Mandatory)][string] $BuildNumber
12 | )
13 |
14 |
15 | # Check if the environment is compatible with the script
16 | #############################################################################
17 | if (-Not (Get-ChildItem -Path $Root -Name package.json)) {
18 | Write-Error "package.json configuration missing from $Path"
19 | exit 1
20 | }
21 | if (-Not (Get-Command npm)) {
22 | Write-Error "Environment missing required command: npm"
23 | exit 1
24 | }
25 |
26 |
27 | try {
28 | Write-Output "TrackJS-Node Production Release Starting..."
29 |
30 | # Get package.json build information for release
31 | #############################################################################
32 | $Package = Get-Content "$Root/package.json" -raw | ConvertFrom-Json
33 | $PackageVersion = $Package.version
34 | Write-Output "Updating package version to $PackageVersion"
35 | Write-Output "##teamcity[buildNumber '$PackageVersion']"
36 |
37 | # Publish to npm
38 | #############################################################################
39 | Write-Output "Publishing to npm"
40 | & npm publish $Root --tag latest
41 | if ($lastExitCode -ne 0) {
42 | Write-Error "Failed to publish to npm"
43 | exit 1;
44 | }
45 |
46 | Write-Output "TrackJS-Node Production Release Complete."
47 | } catch {
48 | Write-Error $_.Exception.Message
49 | exit 1
50 | }
51 |
--------------------------------------------------------------------------------
/tools/teamcity.js:
--------------------------------------------------------------------------------
1 | const package = require("../package.json");
2 |
3 | // Magic string for teamcity info.
4 | // @see https://www.jetbrains.com/help/teamcity/build-script-interaction-with-teamcity.html
5 | console.log('##teamcity[buildNumber \'' + package.version + '-build.{build.number}\']');
6 | console.log('##teamcity[setParameter name=\'env.PACKAGE_VERSION\' value=\'' + package.version + '\']');
7 |
--------------------------------------------------------------------------------
/tools/version.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const git = require("simple-git");
3 | const package = require("../package.json");
4 |
5 | let _hash;
6 | function getGitHash() {
7 | return new Promise((resolve, reject) => {
8 | if (_hash) {
9 | resolve(_hash);
10 | }
11 | git().revparse(["HEAD"], (error, data) => {
12 | if (error) {
13 | reject("Could not get hash", error);
14 | }
15 | _hash = data.trim();
16 | resolve(_hash);
17 | });
18 | });
19 | }
20 |
21 | function writeVersionInfo(files) {
22 | return Promise.all(
23 | (files || []).map(file => {
24 | return new Promise((resolve, reject) => {
25 | if (!fs.existsSync(file)) {
26 | return reject(file + " does not exist.");
27 | }
28 |
29 | fs.readFile(file, "utf8", (error, data) => {
30 | if (error) {
31 | return reject(error);
32 | }
33 | getGitHash()
34 | .then(hash => {
35 | let newFileContent = data
36 | .replace(/%HASH%/g, hash)
37 | .replace(/%VERSION%/g, package.version)
38 | .replace(/%NAME%/g, package.name);
39 | fs.writeFile(file, newFileContent, "utf8", error => {
40 | if (error) {
41 | return reject(error);
42 | }
43 | resolve();
44 | });
45 | })
46 | .catch(err => reject);
47 | });
48 | });
49 | })
50 | );
51 | }
52 |
53 | writeVersionInfo(["./dist/version.js"])
54 | .then(() => {
55 | console.log("version complete");
56 | process.exit(0);
57 | })
58 | .catch(error => {
59 | console.error("version failed", error);
60 | process.exit(1);
61 | });
62 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "declarationDir": "./dist",
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "outDir": "./dist",
8 | "removeComments": true,
9 | "esModuleInterop": true,
10 | "types": ["node","jest"]
11 | },
12 | "include": ["src/**/*"]
13 | }
14 |
--------------------------------------------------------------------------------