@gasket/plugin-elastic-apm 7.0.0-next.7 → 7.0.0-next.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,38 +4,32 @@ Adds Elastic APM instrumentation to your application
4
4
 
5
5
  ## Installation
6
6
 
7
- #### New apps
8
-
9
7
  ```
10
- gasket create <app-name> --plugins @gasket/plugin-elastic-apm
8
+ npm i @gasket/plugin-elastic-apm
11
9
  ```
12
10
 
13
- #### Existing apps
11
+ Update your `gasket` file plugin configuration:
14
12
 
15
- ```
16
- npm install @gasket/plugin-elastic-apm elastic-apm-node
17
- ```
13
+ ```diff
14
+ // gasket.js
18
15
 
19
- Modify `plugins` section of your `gasket.config.js`:
16
+ + import pluginElasticApm from '@gasket/plugin-elastic-apm';
20
17
 
21
- ```diff
22
- module.exports = {
23
- plugins: {
24
- add: [
25
- + '@gasket/plugin-elastic-apm'
26
- ]
27
- }
28
- }
18
+ export default makeGasket({
19
+ plugins: [
20
+ + pluginElasticApm
21
+ ]
22
+ });
29
23
  ```
30
24
 
31
- Add a `--require` flag to a `package.json` start script:
25
+ Add `NODE_OPTIONS=--import=./setup.js` to the `package.json` start script:
32
26
 
33
27
  ```diff
34
28
  "scripts": {
35
- "build": "gasket build",
36
- - "start": "gasket start",
37
- + "start": "gasket start --require ./setup.js",
38
- "local": "gasket local"
29
+ "build": "next build",
30
+ - "start": "next start",
31
+ + "start": "NODE_OPTIONS=--import=./setup.js next start",
32
+ "local": "next dev"
39
33
  }
40
34
  ```
41
35
 
@@ -43,13 +37,16 @@ Add a `setup.js` script to the root of your app
43
37
 
44
38
  ```
45
39
  // setup.js
46
- require('dotenv').config();
40
+ import dotenv from 'dotenv/config';
41
+ import apm from 'elastic-apm-node';
47
42
 
48
- require('elastic-apm-node').start({
43
+ // Elastic APM setup
44
+ apm.start({
49
45
  serviceName: 'my-service-name',
46
+ captureHeaders: false,
50
47
  secretToken: process.env.ELASTIC_APM_SECRET_TOKEN,
51
48
  serverUrl: process.env.ELASTIC_APM_SERVER_URL
52
- // any additional configurations options
49
+ // additional configuration options
53
50
  });
54
51
  ```
55
52
 
@@ -68,7 +65,7 @@ are not present, the APM agent will be disabled.
68
65
  ### Plugin Configurations
69
66
 
70
67
  The Gasket plugin provides some additional setup helpers. These can be
71
- configured under `elasticAPM` in the `gasket.config.js`.
68
+ configured under `elasticAPM` in the `gasket.js`.
72
69
 
73
70
  - **`sensitiveCookies`** - (string[]) A list of sensitive cookies to filter
74
71
 
@@ -77,14 +74,14 @@ configured under `elasticAPM` in the `gasket.config.js`.
77
74
  If your application’s users send session credentials or any other sensitive
78
75
  information in their cookies, you may wish to filter them out before they are
79
76
  stored in Elasticsearch. Specify a list of cookie names to redact in
80
- `gasket.config.js`:
77
+ `gasket.js`:
81
78
 
82
79
  ```js
83
- module.exports = {
80
+ export default makeGasket({
84
81
  elasticAPM: {
85
82
  sensitiveCookies: ['my_jwt', 'userFullName']
86
83
  }
87
- };
84
+ });
88
85
  ```
89
86
 
90
87
  #### Custom Filtering Sensitive Fields
@@ -117,6 +114,35 @@ According to the [Elastic APM docs], the _Elastic APM agent for Node.js is a
117
114
  singleton_. This means that you can require and configure singleton in various
118
115
  hooks of your Gasket app, such as with the [init] or [middleware] lifecycles.
119
116
 
117
+ ## Actions
118
+
119
+ ### getApmTransaction
120
+
121
+ Use the `getApmTransaction` action to access and decorate the current APM
122
+ transaction. This action is available in any lifecycle hook or server-side code.
123
+
124
+ ```js
125
+ // example-plugin.js
126
+
127
+ export default {
128
+ name: 'example-plugin',
129
+ hooks: {
130
+ express(gasket, app) {
131
+ app.use(async (req, res, next) => {
132
+ const transaction = await gasket.actions.getApmTransaction(req);
133
+ const locale = await gasket.actions.getIntlLocale(req);
134
+ transaction.setLabel('locale', locale);
135
+ });
136
+ }
137
+ }
138
+ }
139
+ ```
140
+
141
+ In the above example, we are hooking the express lifecycle to add middleware
142
+ to decorate the transaction.
143
+ Calling `getApmTransaction` will also allow other plugins to decorate the
144
+ transaction by hooking the `apmTransaction` lifecycle discussed next.
145
+
120
146
  ## Lifecycles
121
147
 
122
148
  ### apmTransaction
@@ -131,20 +157,22 @@ request details are as follows:
131
157
  | `res` | The HTTP response or framework-specific wrapper around it |
132
158
 
133
159
  ```javascript
134
- // /lifecycles/apm-transaction.js
135
-
136
- module.exports = (gasket, transaction, { req, res }) => {
137
- transaction.setLabel('language', req.headers['accept-language']);
160
+ // example-plugin.js
161
+
162
+ export default {
163
+ name: 'example-plugin',
164
+ hooks: {
165
+ apmTransaction(gasket, transaction, { req, res }) => {
166
+ transaction.setLabel('language', req.headers['accept-language']);
167
+ }
168
+ }
138
169
  }
139
170
  ```
140
171
 
141
172
  ## How it works
142
173
 
143
- This plugin hooks the Gasket [preboot] lifecycle from [@gasket/plugin-start] and
144
- will set up additional filtering, such as for sensitive cookies. If the
145
- `preboot` hook finds that the APM agent has not yet been started using the
146
- recommended `--require elastic-apm-node/start`, it will start it here. However,
147
- you risk not bootstrapping necessary modules with a late start.
174
+ This plugin hooks the Gasket [configure] lifecycle to set additional filtering,
175
+ such as for sensitive cookies.
148
176
 
149
177
  ## License
150
178
 
package/lib/actions.js ADDED
@@ -0,0 +1,21 @@
1
+ /** @type {import('@gasket/core').ActionHandler<'getApmTransaction'>} */
2
+ async function getApmTransaction(gasket, req) {
3
+ const apm = require('elastic-apm-node');
4
+
5
+ if (!apm?.isStarted()) {
6
+ return;
7
+ }
8
+
9
+ const transaction = apm.currentTransaction;
10
+ if (!transaction) {
11
+ return;
12
+ }
13
+
14
+ await gasket.exec('apmTransaction', transaction, { req });
15
+
16
+ return transaction;
17
+ }
18
+
19
+ module.exports = {
20
+ getApmTransaction
21
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Determines if the Elastic APM agent has sufficient config to be active
3
+ * @param {import('./index').ExtendedAgentConfigOptions} config Apm agent config
4
+ * @param {NodeJS.ProcessEnv} env Environment variables
5
+ * @returns {boolean} A combined config object
6
+ */
7
+ const isActive = (config, env) => {
8
+ const { active, serverUrl, secretToken } = config;
9
+
10
+ if (active || env.ELASTIC_APM_ACTIVE) {
11
+ return true;
12
+ }
13
+
14
+ const combined = {
15
+ serverUrl: serverUrl || env.ELASTIC_APM_SERVER_URL,
16
+ secretToken: secretToken || env.ELASTIC_APM_SECRET_TOKEN
17
+ };
18
+
19
+ if (combined.serverUrl && combined.secretToken) {
20
+ return true;
21
+ }
22
+
23
+ return false;
24
+ };
25
+
26
+ /** @type {import('@gasket/core').HookHandler<'configure'>} */
27
+ module.exports = function configure(gasket, config) {
28
+ config.elasticAPM = config.elasticAPM || {};
29
+
30
+ // eslint-disable-next-line no-process-env
31
+ config.elasticAPM.active = isActive(config.elasticAPM, process.env);
32
+
33
+ return { ...config };
34
+ };
package/lib/cookies.js CHANGED
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Returns an array of cookie names which are considered sensitive because they
3
3
  * may contain session credential or PII
4
- *
5
- * @param {*} config the Gasket config object
4
+ * @param {import('@gasket/core').GasketConfig} config the Gasket config
6
5
  * @returns {string[]} an array of cookie names
7
6
  */
8
7
  const sensitiveCookies = (config) => {
@@ -18,40 +17,42 @@ const sensitiveCookies = (config) => {
18
17
 
19
18
  /**
20
19
  * Redacts the contents of user-specified sensitive cookies
21
- *
22
- * @param {object} config The Gasket config
23
- * @param {object} payload The APM payload
24
- * @returns {object} a modified version of the incoming APM payload
20
+ * @type {import('./index').filterSensitiveCookies}
25
21
  */
26
- const filterSensitiveCookies = (config) => (payload) => {
27
- const cookiesToRedact = sensitiveCookies(config);
22
+ const filterSensitiveCookies = function (config) {
23
+ return function (payload) {
24
+ const cookiesToRedact = sensitiveCookies(config);
28
25
 
29
- if (
30
- payload.context &&
26
+ if (
27
+ payload.context &&
31
28
  payload.context.request &&
32
29
  payload.context.request.headers &&
33
30
  payload.context.request.headers.cookie
34
- ) {
35
- let cookie = payload.context.request.headers.cookie;
36
- cookiesToRedact.forEach((sc) => {
37
- cookie = cookie.replace(new RegExp(sc + '=([^;]+)'), sc + '=[REDACTED]');
38
- });
31
+ ) {
32
+ let cookie = payload.context.request.headers.cookie;
33
+ cookiesToRedact.forEach((sc) => {
34
+ cookie = cookie.replace(
35
+ new RegExp(sc + '=([^;]+)'),
36
+ sc + '=[REDACTED]'
37
+ );
38
+ });
39
39
 
40
- payload.context.request.headers.cookie = cookie;
41
- }
40
+ payload.context.request.headers.cookie = cookie;
41
+ }
42
42
 
43
- if (payload.context &&
43
+ if (payload.context &&
44
44
  payload.context.request &&
45
45
  payload.context.request.cookies
46
- ) {
47
- cookiesToRedact.forEach((sc) => {
48
- if (sc in payload.context.request.cookies) {
49
- payload.context.request.cookies[sc] = '[REDACTED]';
50
- }
51
- });
52
- }
46
+ ) {
47
+ cookiesToRedact.forEach((sc) => {
48
+ if (sc in payload.context.request.cookies) {
49
+ payload.context.request.cookies[sc] = '[REDACTED]';
50
+ }
51
+ });
52
+ }
53
53
 
54
- return payload;
54
+ return payload;
55
+ };
55
56
  };
56
57
 
57
58
  module.exports = {
package/lib/create.js ADDED
@@ -0,0 +1,24 @@
1
+ const { name, version, devDependencies } = require('../package.json');
2
+
3
+ /** @type {import('@gasket/core').HookHandler<'create'>} */
4
+ module.exports = function create(gasket, { pkg, files, gasketConfig }) {
5
+ const generatorDir = `${__dirname}/../generator`;
6
+
7
+ gasketConfig.addPlugin('pluginElasticApm', name);
8
+
9
+ pkg.add('dependencies', {
10
+ [name]: `^${version}`,
11
+ 'dotenv': devDependencies.dotenv,
12
+ 'elastic-apm-node': devDependencies['elastic-apm-node']
13
+ });
14
+
15
+ pkg.extend((current) => {
16
+ return {
17
+ scripts: {
18
+ start: `NODE_OPTIONS=--import=./setup.js ${current.scripts.start}`
19
+ }
20
+ };
21
+ });
22
+
23
+ files.add(`${generatorDir}/*`);
24
+ };
package/lib/index.d.ts CHANGED
@@ -1,25 +1,42 @@
1
- import type { IncomingMessage, ServerResponse } from 'http';
2
- import type { AgentConfigOptions, Transaction } from 'elastic-apm-node';
3
- import type { MaybeAsync } from '@gasket/engine';
1
+ import type { IncomingMessage } from 'http';
2
+ import type { AgentConfigOptions, Transaction, Payload, Agent } from 'elastic-apm-node';
3
+ import type { MaybeAsync, GasketConfig, Plugin } from '@gasket/core';
4
+ import type { Request } from 'express';
4
5
 
5
- declare module '@gasket/engine' {
6
+ export function filterSensitiveCookies(config: GasketConfig): function(Payload): Payload;
7
+
8
+ export interface ExtendedAgentConfigOptions extends AgentConfigOptions {
9
+ sensitiveCookies?: Array<string>;
10
+ }
11
+
12
+ declare module '@gasket/core' {
6
13
  export interface GasketConfig {
7
- elasticAPM?: AgentConfigOptions & {
8
- sensitiveCookies?: Array<string>
9
- },
14
+ elasticAPM?: ExtendedAgentConfigOptions;
10
15
  }
11
16
 
12
17
  export interface Gasket {
13
18
  apm?: Agent;
14
19
  }
15
20
 
21
+ export interface GasketActions {
22
+ async getApmTransaction(
23
+ req: IncomingMessage | Request
24
+ ): Promise<Transaction | void>
25
+ }
26
+
16
27
  export interface HookExecTypes {
17
28
  apmTransaction(
18
29
  transaction: Transaction,
19
- details: {
20
- req: IncomingMessage,
21
- res: ServerResponse
30
+ context: {
31
+ req: IncomingMessage | Request;
22
32
  }
23
- ): MaybeAsync<void>
33
+ ): MaybeAsync<void>;
24
34
  }
25
35
  }
36
+
37
+ const plugin: Plugin = {
38
+ name: '@gasket/plugin-elastic-apm',
39
+ hooks: {}
40
+ };
41
+
42
+ export = plugin;
package/lib/index.js CHANGED
@@ -1,81 +1,34 @@
1
- const { filterSensitiveCookies } = require('./cookies');
2
- const middleware = require('./middleware');
3
- const { devDependencies } = require('../package.json');
4
-
5
- /**
6
- * Determines if the Elastic APM agent has sufficient config to be active
7
- * @param {object} config gasket config
8
- * @param {object<string,any>} env environment variables
9
- * @returns {boolean} A combined config object
10
- */
11
- const isActive = (config, env) => {
12
- const { active } = config;
13
-
14
- if (active || env.ELASTIC_APM_ACTIVE) {
15
- return true;
16
- }
17
-
18
- if (env.ELASTIC_APM_SERVER_URL && env.ELASTIC_APM_SECRET_TOKEN) {
19
- return true;
20
- }
21
-
22
- return false;
23
- };
24
-
25
- module.exports = {
1
+ /// <reference types="create-gasket-app" />
2
+ /// <reference types="@gasket/plugin-metadata" />
3
+
4
+ const actions = require('./actions');
5
+ const create = require('./create');
6
+ const configure = require('./configure');
7
+ const {
8
+ name,
9
+ version,
10
+ description
11
+ } = require('../package.json');
12
+
13
+ /** @type {import('@gasket/core').Plugin} */
14
+ const plugin = {
15
+ name,
16
+ version,
17
+ description,
18
+ actions,
26
19
  hooks: {
27
- configure: {
28
- handler: (gasket, config) => {
29
- config.elasticAPM = config.elasticAPM || {};
30
-
31
- // eslint-disable-next-line no-process-env
32
- config.elasticAPM.active = isActive(config.elasticAPM, process.env);
33
-
34
- return { ...config };
35
- }
36
- },
37
- preboot: {
38
- handler: async (gasket) => {
39
- const { config, logger, command } = gasket;
40
-
41
- if (command && command.id === 'local') return;
42
-
43
- // prefer app-level dependency in case of duplicates
44
- const apm = require(require.resolve('elastic-apm-node', {
45
- paths: [config.root, __dirname]
46
- }));
47
-
48
- if (!apm.isStarted()) {
49
- logger.warn(
50
- 'Elastic APM agent is not started. Use `--require ./setup.js`'
51
- );
52
- }
53
-
54
- apm.addFilter(filterSensitiveCookies(config));
55
- }
56
- },
57
- create: {
58
- timing: {
59
- after: ['@gasket/plugin-start']
60
- },
61
- handler(gasket, { pkg, files }) {
62
- const generatorDir = `${__dirname}/../generator`;
63
-
64
- pkg.add('dependencies', {
65
- 'elastic-apm-node': devDependencies['elastic-apm-node'],
66
- 'dotenv': devDependencies.dotenv
67
- });
68
- pkg.add('scripts', {
69
- start: 'gasket start --require ./setup.js'
70
- });
71
-
72
- files.add(`${generatorDir}/*`);
73
- }
74
- },
75
- middleware,
20
+ configure,
21
+ create,
76
22
  metadata(gasket, meta) {
77
23
  return {
78
24
  ...meta,
25
+ actions: [
26
+ {
27
+ name: 'getApmTransaction',
28
+ description: 'Get the APM transaction data',
29
+ link: 'README.md#getApmTransaction'
30
+ }
31
+ ],
79
32
  configurations: [
80
33
  {
81
34
  name: 'elasticAPM',
@@ -104,3 +57,5 @@ module.exports = {
104
57
  }
105
58
  }
106
59
  };
60
+
61
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@gasket/plugin-elastic-apm",
3
- "version": "7.0.0-next.7",
3
+ "version": "7.0.0-next.70",
4
4
  "description": "Adds Elastic APM instrumentation to your application",
5
- "main": "lib",
5
+ "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
7
7
  "files": [
8
8
  "lib"
@@ -10,12 +10,12 @@
10
10
  "scripts": {
11
11
  "lint": "eslint .",
12
12
  "lint:fix": "npm run lint -- --fix",
13
- "test": "npm run test:runner",
14
- "test:runner": "jest test/*.test.js",
15
- "test:watch": "npm run test:runner -- --watch",
16
- "test:coverage": "npm run test:runner -- --coverage --coverageReporters text",
17
- "posttest": "npm run lint",
18
- "report": "npm run test:runner -- --coverage --coverageReporters lcov"
13
+ "test": "cross-env NODE_OPTIONS='--unhandled-rejections=strict' jest",
14
+ "test:watch": "npm run test -- --watch",
15
+ "test:coverage": "npm run test -- --coverage",
16
+ "posttest": "npm run lint && npm run typecheck",
17
+ "typecheck": "tsc",
18
+ "typecheck:watch": "tsc --watch"
19
19
  },
20
20
  "repository": {
21
21
  "type": "git",
@@ -31,33 +31,33 @@
31
31
  "plugin"
32
32
  ],
33
33
  "author": "GoDaddy Operating Company, LLC",
34
- "maintainers": [
35
- "Keith Bartholomew <kbartholomew@godaddy.com>"
36
- ],
37
34
  "license": "MIT",
38
35
  "bugs": {
39
36
  "url": "https://github.com/godaddy/gasket/issues"
40
37
  },
41
38
  "homepage": "https://github.com/godaddy/gasket/tree/main/packages/gasket-plugin-elastic-apm",
42
39
  "devDependencies": {
43
- "@gasket/engine": "^7.0.0-next.7",
44
- "dot-env": "^0.0.1",
40
+ "@gasket/core": "7.0.0-next.70",
41
+ "dotenv": "^16.4.5",
45
42
  "elastic-apm-node": "^4.4.1",
46
43
  "eslint": "^8.56.0",
47
- "eslint-config-godaddy": "^7.0.2",
48
- "eslint-plugin-jest": "^27.6.3",
44
+ "eslint-config-godaddy": "^7.1.1",
45
+ "eslint-plugin-jest": "^28.6.0",
49
46
  "eslint-plugin-json": "^3.1.0",
50
- "eslint-plugin-unicorn": "^44.0.0",
47
+ "eslint-plugin-unicorn": "^55.0.0",
51
48
  "jest": "^29.7.0",
52
- "nyc": "^15.1.0"
49
+ "nyc": "^15.1.0",
50
+ "typescript": "^5.4.5"
53
51
  },
54
52
  "eslintConfig": {
55
53
  "extends": [
56
54
  "godaddy",
57
- "plugin:jest/recommended"
55
+ "plugin:jest/recommended",
56
+ "plugin:jsdoc/recommended-typescript-flavor"
58
57
  ],
59
58
  "plugins": [
60
- "unicorn"
59
+ "unicorn",
60
+ "jsdoc"
61
61
  ],
62
62
  "rules": {
63
63
  "unicorn/filename-case": "error"
@@ -66,5 +66,5 @@
66
66
  "jest": {
67
67
  "testEnvironment": "node"
68
68
  },
69
- "gitHead": "de14ea652e392f002c291d1e441ccf836f5fddb2"
69
+ "gitHead": "f261bf98ca587e646dc61fa49dd50859a0f43a47"
70
70
  }
package/lib/middleware.js DELETED
@@ -1,41 +0,0 @@
1
- /* eslint-disable spaced-comment */
2
- // @ts-check
3
- /// <reference types="./index" />
4
- /// <reference types="../../gasket-plugin-nextjs" />
5
-
6
- /**
7
- * @typedef {import('@gasket/engine').Gasket} Gasket
8
- * @typedef {import('http').IncomingMessage} Request
9
- * @typedef {import('http').ServerResponse} Response
10
- */
11
-
12
- const { callbackify } = require('util');
13
-
14
- /**
15
- * Middleware for customizing transactions
16
- *
17
- * @param {Gasket} gasket The Gasket engine
18
- * @param {Request} req The HTTP request being handled
19
- * @param {Response} res The server response
20
- */
21
- async function customizeTransaction(gasket, req, res) {
22
- const apm = gasket.apm;
23
-
24
- if (!apm?.isStarted()) {
25
- return;
26
- }
27
-
28
- const transaction = apm.currentTransaction;
29
- if (!transaction) {
30
- return;
31
- }
32
-
33
- await gasket.exec('apmTransaction', transaction, { req, res });
34
- }
35
-
36
- module.exports = (gasket) => {
37
- return (
38
- gasket.apm
39
- && [callbackify(async (req, res) => customizeTransaction(gasket, req, res))]
40
- );
41
- };