@gasket/plugin-elastic-apm 7.0.0-next.5 → 7.0.0-next.50
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 +22 -23
- package/lib/actions.js +21 -0
- package/lib/configure.js +34 -0
- package/lib/cookies.js +27 -26
- package/lib/create.js +24 -0
- package/lib/index.d.ts +29 -10
- package/lib/index.js +22 -72
- package/lib/middleware.js +23 -23
- package/package.json +17 -13
package/README.md
CHANGED
|
@@ -4,28 +4,22 @@ Adds Elastic APM instrumentation to your application
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
#### New apps
|
|
8
|
-
|
|
9
7
|
```
|
|
10
|
-
|
|
8
|
+
npm i @gasket/plugin-elastic-apm
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
Update your `gasket` file plugin configuration:
|
|
14
12
|
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
```
|
|
13
|
+
```diff
|
|
14
|
+
// gasket.js
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
+ import pluginElasticApm from '@gasket/plugin-elastic-apm';
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
]
|
|
27
|
-
}
|
|
28
|
-
}
|
|
18
|
+
export default makeGasket({
|
|
19
|
+
plugins: [
|
|
20
|
+
+ pluginElasticApm
|
|
21
|
+
]
|
|
22
|
+
});
|
|
29
23
|
```
|
|
30
24
|
|
|
31
25
|
Add a `--require` flag to a `package.json` start script:
|
|
@@ -77,14 +71,14 @@ configured under `elasticAPM` in the `gasket.config.js`.
|
|
|
77
71
|
If your application’s users send session credentials or any other sensitive
|
|
78
72
|
information in their cookies, you may wish to filter them out before they are
|
|
79
73
|
stored in Elasticsearch. Specify a list of cookie names to redact in
|
|
80
|
-
`gasket.
|
|
74
|
+
`gasket.js`:
|
|
81
75
|
|
|
82
76
|
```js
|
|
83
|
-
|
|
77
|
+
export default makeGasket({
|
|
84
78
|
elasticAPM: {
|
|
85
79
|
sensitiveCookies: ['my_jwt', 'userFullName']
|
|
86
80
|
}
|
|
87
|
-
};
|
|
81
|
+
});
|
|
88
82
|
```
|
|
89
83
|
|
|
90
84
|
#### Custom Filtering Sensitive Fields
|
|
@@ -131,10 +125,15 @@ request details are as follows:
|
|
|
131
125
|
| `res` | The HTTP response or framework-specific wrapper around it |
|
|
132
126
|
|
|
133
127
|
```javascript
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
128
|
+
// example-plugin.js
|
|
129
|
+
|
|
130
|
+
export default {
|
|
131
|
+
name: 'example-plugin',
|
|
132
|
+
hooks: {
|
|
133
|
+
apmTransaction(gasket, transaction, { req, res }) => {
|
|
134
|
+
transaction.setLabel('language', req.headers['accept-language']);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
138
137
|
}
|
|
139
138
|
```
|
|
140
139
|
|
package/lib/actions.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** @type {import('@gasket/core').HookHandler<'actions'>} */
|
|
2
|
+
module.exports = function actions(gasket) {
|
|
3
|
+
return {
|
|
4
|
+
async getApmTransaction(req) {
|
|
5
|
+
const apm = require('elastic-apm-node');
|
|
6
|
+
|
|
7
|
+
if (!apm?.isStarted()) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const transaction = apm.currentTransaction;
|
|
12
|
+
if (!transaction) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
await gasket.exec('apmTransaction', transaction, { req });
|
|
17
|
+
|
|
18
|
+
return transaction;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
};
|
package/lib/configure.js
ADDED
|
@@ -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)
|
|
27
|
-
|
|
22
|
+
const filterSensitiveCookies = function (config) {
|
|
23
|
+
return function (payload) {
|
|
24
|
+
const cookiesToRedact = sensitiveCookies(config);
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
40
|
+
payload.context.request.headers.cookie = cookie;
|
|
41
|
+
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
if (payload.context &&
|
|
44
44
|
payload.context.request &&
|
|
45
45
|
payload.context.request.cookies
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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,44 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
-
import type { AgentConfigOptions, Transaction } from 'elastic-apm-node';
|
|
3
|
-
import type { MaybeAsync } from '@gasket/
|
|
2
|
+
import type { AgentConfigOptions, Transaction, Payload } from 'elastic-apm-node';
|
|
3
|
+
import type { MaybeAsync, GasketConfig } from '@gasket/core';
|
|
4
|
+
import type { GasketData } from '@gasket/data';
|
|
5
|
+
import type { GasketRequest } from '@gasket/core';
|
|
6
|
+
import type { Request } from 'express';
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
export function filterSensitiveCookies(config: GasketConfig): function(Payload): Payload;
|
|
9
|
+
|
|
10
|
+
export interface ExtendedAgentConfigOptions extends AgentConfigOptions {
|
|
11
|
+
sensitiveCookies?: Array<string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare module '@gasket/core' {
|
|
6
15
|
export interface GasketConfig {
|
|
7
|
-
elasticAPM?:
|
|
8
|
-
sensitiveCookies?: Array<string>
|
|
9
|
-
},
|
|
16
|
+
elasticAPM?: ExtendedAgentConfigOptions;
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
export interface Gasket {
|
|
13
20
|
apm?: Agent;
|
|
14
21
|
}
|
|
15
22
|
|
|
23
|
+
export interface GasketActions {
|
|
24
|
+
async getApmTransaction(
|
|
25
|
+
req: IncomingMessage | Request
|
|
26
|
+
): Promise<Transaction | void>
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
export interface HookExecTypes {
|
|
17
30
|
apmTransaction(
|
|
18
31
|
transaction: Transaction,
|
|
19
|
-
|
|
20
|
-
req: IncomingMessage
|
|
21
|
-
res: ServerResponse
|
|
32
|
+
context: {
|
|
33
|
+
req: IncomingMessage | Request;
|
|
22
34
|
}
|
|
23
|
-
): MaybeAsync<void
|
|
35
|
+
): MaybeAsync<void>;
|
|
24
36
|
}
|
|
25
37
|
}
|
|
38
|
+
|
|
39
|
+
export default {
|
|
40
|
+
name: '@gasket/plugin-elastic-apm',
|
|
41
|
+
version: '',
|
|
42
|
+
description: '',
|
|
43
|
+
hooks: {}
|
|
44
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -1,77 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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;
|
|
1
|
+
/// <reference types="create-gasket-app" />
|
|
2
|
+
/// <reference types="@gasket/plugin-metadata" />
|
|
13
3
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
4
|
+
const actions = require('./actions');
|
|
5
|
+
const create = require('./create');
|
|
6
|
+
const middleware = require('./middleware');
|
|
7
|
+
const configure = require('./configure');
|
|
8
|
+
const {
|
|
9
|
+
name,
|
|
10
|
+
version,
|
|
11
|
+
description
|
|
12
|
+
} = require('../package.json');
|
|
13
|
+
|
|
14
|
+
/** @type {import('@gasket/core').Plugin} */
|
|
15
|
+
const plugin = {
|
|
16
|
+
name,
|
|
17
|
+
version,
|
|
18
|
+
description,
|
|
26
19
|
hooks: {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
},
|
|
20
|
+
actions,
|
|
21
|
+
configure,
|
|
22
|
+
create,
|
|
75
23
|
middleware,
|
|
76
24
|
metadata(gasket, meta) {
|
|
77
25
|
return {
|
|
@@ -104,3 +52,5 @@ module.exports = {
|
|
|
104
52
|
}
|
|
105
53
|
}
|
|
106
54
|
};
|
|
55
|
+
|
|
56
|
+
module.exports = plugin;
|
package/lib/middleware.js
CHANGED
|
@@ -1,26 +1,12 @@
|
|
|
1
|
-
|
|
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');
|
|
1
|
+
/// <reference types="@gasket/plugin-express" />
|
|
13
2
|
|
|
14
3
|
/**
|
|
15
4
|
* Middleware for customizing transactions
|
|
16
|
-
*
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {Request} req The HTTP request being handled
|
|
19
|
-
* @param {Response} res The server response
|
|
5
|
+
* @param {import('@gasket/core').Gasket} gasket - The Gasket engine
|
|
6
|
+
* @param {import('http').IncomingMessage} req - The HTTP request being handled
|
|
20
7
|
*/
|
|
21
|
-
async function customizeTransaction(gasket, req
|
|
22
|
-
const apm =
|
|
23
|
-
|
|
8
|
+
async function customizeTransaction(gasket, req) {
|
|
9
|
+
const apm = require('elastic-apm-node');
|
|
24
10
|
if (!apm?.isStarted()) {
|
|
25
11
|
return;
|
|
26
12
|
}
|
|
@@ -30,12 +16,26 @@ async function customizeTransaction(gasket, req, res) {
|
|
|
30
16
|
return;
|
|
31
17
|
}
|
|
32
18
|
|
|
33
|
-
await gasket.exec('apmTransaction', transaction, { req
|
|
19
|
+
await gasket.exec('apmTransaction', transaction, { req });
|
|
34
20
|
}
|
|
35
21
|
|
|
36
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Add middleware to gather config details
|
|
24
|
+
* @type {import('@gasket/core').HookHandler<'middleware'>}
|
|
25
|
+
*/
|
|
26
|
+
module.exports = function middleware(gasket) {
|
|
27
|
+
const apm = require('elastic-apm-node');
|
|
28
|
+
// eslint-disable-next-line no-undefined
|
|
29
|
+
const isStarted = apm.isStarted() || undefined;
|
|
37
30
|
return (
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
isStarted &&
|
|
32
|
+
async function apmTransactionMiddleware(req, res, next) {
|
|
33
|
+
try {
|
|
34
|
+
customizeTransaction(gasket, req);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return next(error);
|
|
37
|
+
}
|
|
38
|
+
next();
|
|
39
|
+
}
|
|
40
40
|
);
|
|
41
41
|
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gasket/plugin-elastic-apm",
|
|
3
|
-
"version": "7.0.0-next.
|
|
3
|
+
"version": "7.0.0-next.50",
|
|
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"
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
"test:runner": "jest test/*.test.js",
|
|
15
15
|
"test:watch": "npm run test:runner -- --watch",
|
|
16
16
|
"test:coverage": "npm run test:runner -- --coverage --coverageReporters text",
|
|
17
|
-
"posttest": "npm run lint",
|
|
18
|
-
"
|
|
17
|
+
"posttest": "npm run lint && npm run typecheck",
|
|
18
|
+
"typecheck": "tsc",
|
|
19
|
+
"typecheck:watch": "tsc --watch"
|
|
19
20
|
},
|
|
20
21
|
"repository": {
|
|
21
22
|
"type": "git",
|
|
@@ -40,24 +41,27 @@
|
|
|
40
41
|
},
|
|
41
42
|
"homepage": "https://github.com/godaddy/gasket/tree/main/packages/gasket-plugin-elastic-apm",
|
|
42
43
|
"devDependencies": {
|
|
43
|
-
"@gasket/
|
|
44
|
-
"
|
|
44
|
+
"@gasket/core": "^7.0.0-next.50",
|
|
45
|
+
"dotenv": "^16.4.5",
|
|
45
46
|
"elastic-apm-node": "^4.4.1",
|
|
46
47
|
"eslint": "^8.56.0",
|
|
47
|
-
"eslint-config-godaddy": "^7.
|
|
48
|
-
"eslint-plugin-jest": "^
|
|
48
|
+
"eslint-config-godaddy": "^7.1.1",
|
|
49
|
+
"eslint-plugin-jest": "^28.6.0",
|
|
49
50
|
"eslint-plugin-json": "^3.1.0",
|
|
50
|
-
"eslint-plugin-unicorn": "^
|
|
51
|
+
"eslint-plugin-unicorn": "^55.0.0",
|
|
51
52
|
"jest": "^29.7.0",
|
|
52
|
-
"nyc": "^15.1.0"
|
|
53
|
+
"nyc": "^15.1.0",
|
|
54
|
+
"typescript": "^5.4.5"
|
|
53
55
|
},
|
|
54
56
|
"eslintConfig": {
|
|
55
57
|
"extends": [
|
|
56
58
|
"godaddy",
|
|
57
|
-
"plugin:jest/recommended"
|
|
59
|
+
"plugin:jest/recommended",
|
|
60
|
+
"plugin:jsdoc/recommended-typescript-flavor"
|
|
58
61
|
],
|
|
59
62
|
"plugins": [
|
|
60
|
-
"unicorn"
|
|
63
|
+
"unicorn",
|
|
64
|
+
"jsdoc"
|
|
61
65
|
],
|
|
62
66
|
"rules": {
|
|
63
67
|
"unicorn/filename-case": "error"
|
|
@@ -66,5 +70,5 @@
|
|
|
66
70
|
"jest": {
|
|
67
71
|
"testEnvironment": "node"
|
|
68
72
|
},
|
|
69
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "e7cfec6a118cf087c6532c23dc31170f4b15d182"
|
|
70
74
|
}
|