@netlify/build 29.7.2 → 29.9.0
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/lib/core/build.js +16 -5
- package/lib/core/main.js +5 -2
- package/lib/core/report_metrics.js +17 -0
- package/lib/plugins_core/edge_functions/index.js +18 -2
- package/lib/plugins_core/edge_functions/validate_manifest/validate_edge_functions_manifest.js +2 -9
- package/lib/plugins_core/functions/index.js +18 -0
- package/lib/steps/core_step.js +2 -1
- package/lib/steps/return.js +2 -2
- package/lib/steps/run_step.js +2 -1
- package/lib/steps/run_steps.js +5 -2
- package/package.json +2 -2
package/lib/core/build.js
CHANGED
|
@@ -89,7 +89,7 @@ const tExecBuild = async function ({ config, defaultConfig, cachedConfig, cached
|
|
|
89
89
|
const pluginsOptions = addCorePlugins({ netlifyConfig, constants });
|
|
90
90
|
// `errorParams` is purposely stateful
|
|
91
91
|
Object.assign(errorParams, { netlifyConfig, pluginsOptions, siteInfo, childEnv, userNodeVersion });
|
|
92
|
-
const { pluginsOptions: pluginsOptionsA, netlifyConfig: netlifyConfigA, stepsCount, timers: timersB, configMutations, } = await runAndReportBuild({
|
|
92
|
+
const { pluginsOptions: pluginsOptionsA, netlifyConfig: netlifyConfigA, stepsCount, timers: timersB, configMutations, metrics, } = await runAndReportBuild({
|
|
93
93
|
pluginsOptions,
|
|
94
94
|
netlifyConfig,
|
|
95
95
|
configOpts,
|
|
@@ -135,13 +135,14 @@ const tExecBuild = async function ({ config, defaultConfig, cachedConfig, cached
|
|
|
135
135
|
stepsCount,
|
|
136
136
|
timers: timersB,
|
|
137
137
|
configMutations,
|
|
138
|
+
metrics,
|
|
138
139
|
};
|
|
139
140
|
};
|
|
140
141
|
export const execBuild = measureDuration(tExecBuild, 'total', { parentTag: 'build_site' });
|
|
141
142
|
// Runs a build then report any plugin statuses
|
|
142
143
|
export const runAndReportBuild = async function ({ pluginsOptions, netlifyConfig, configOpts, siteInfo, configPath, outputConfigPath, headersPath, redirectsPath, buildDir, repositoryRoot, nodePath, packageJson, userNodeVersion, childEnv, context, branch, buildbotServerSocket, constants, dry, mode, api, errorMonitor, deployId, errorParams, logs, debug, systemLog, verbose, timers, sendStatus, saveConfig, testOpts, featureFlags, timeline, devCommand, quiet, }) {
|
|
143
144
|
try {
|
|
144
|
-
const { stepsCount, netlifyConfig: netlifyConfigA, statuses, pluginsOptions: pluginsOptionsA, failedPlugins, timers: timersA, configMutations, } = await initAndRunBuild({
|
|
145
|
+
const { stepsCount, netlifyConfig: netlifyConfigA, statuses, pluginsOptions: pluginsOptionsA, failedPlugins, timers: timersA, configMutations, metrics, } = await initAndRunBuild({
|
|
145
146
|
pluginsOptions,
|
|
146
147
|
netlifyConfig,
|
|
147
148
|
configOpts,
|
|
@@ -215,6 +216,7 @@ export const runAndReportBuild = async function ({ pluginsOptions, netlifyConfig
|
|
|
215
216
|
stepsCount,
|
|
216
217
|
timers: timersA,
|
|
217
218
|
configMutations,
|
|
219
|
+
metrics,
|
|
218
220
|
};
|
|
219
221
|
}
|
|
220
222
|
catch (error) {
|
|
@@ -267,7 +269,7 @@ const initAndRunBuild = async function ({ pluginsOptions, netlifyConfig, configO
|
|
|
267
269
|
quiet,
|
|
268
270
|
});
|
|
269
271
|
try {
|
|
270
|
-
const { stepsCount, netlifyConfig: netlifyConfigA, statuses, failedPlugins, timers: timersC, configMutations, } = await runBuild({
|
|
272
|
+
const { stepsCount, netlifyConfig: netlifyConfigA, statuses, failedPlugins, timers: timersC, configMutations, metrics, } = await runBuild({
|
|
271
273
|
childProcesses,
|
|
272
274
|
pluginsOptions: pluginsOptionsA,
|
|
273
275
|
netlifyConfig,
|
|
@@ -315,6 +317,7 @@ const initAndRunBuild = async function ({ pluginsOptions, netlifyConfig, configO
|
|
|
315
317
|
failedPlugins,
|
|
316
318
|
timers: timersC,
|
|
317
319
|
configMutations,
|
|
320
|
+
metrics,
|
|
318
321
|
};
|
|
319
322
|
}
|
|
320
323
|
finally {
|
|
@@ -343,7 +346,7 @@ const runBuild = async function ({ childProcesses, pluginsOptions, netlifyConfig
|
|
|
343
346
|
await doDryRun({ buildDir, steps, netlifyConfig, constants, buildbotServerSocket, logs });
|
|
344
347
|
return { netlifyConfig };
|
|
345
348
|
}
|
|
346
|
-
const { stepsCount, netlifyConfig: netlifyConfigA, statuses, failedPlugins, timers: timersB, configMutations, } = await runSteps({
|
|
349
|
+
const { stepsCount, netlifyConfig: netlifyConfigA, statuses, failedPlugins, timers: timersB, configMutations, metrics, } = await runSteps({
|
|
347
350
|
steps,
|
|
348
351
|
buildbotServerSocket,
|
|
349
352
|
events,
|
|
@@ -375,5 +378,13 @@ const runBuild = async function ({ childProcesses, pluginsOptions, netlifyConfig
|
|
|
375
378
|
featureFlags,
|
|
376
379
|
quiet,
|
|
377
380
|
});
|
|
378
|
-
return {
|
|
381
|
+
return {
|
|
382
|
+
stepsCount,
|
|
383
|
+
netlifyConfig: netlifyConfigA,
|
|
384
|
+
statuses,
|
|
385
|
+
failedPlugins,
|
|
386
|
+
timers: timersB,
|
|
387
|
+
configMutations,
|
|
388
|
+
metrics,
|
|
389
|
+
};
|
|
379
390
|
};
|
package/lib/core/main.js
CHANGED
|
@@ -5,6 +5,7 @@ import { logTimer, logBuildSuccess } from '../log/messages/core.js';
|
|
|
5
5
|
import { trackBuildComplete } from '../telemetry/main.js';
|
|
6
6
|
import { reportTimers } from '../time/report.js';
|
|
7
7
|
import { execBuild, startBuild } from './build.js';
|
|
8
|
+
import { reportMetrics } from './report_metrics.js';
|
|
8
9
|
import { getSeverity } from './severity.js';
|
|
9
10
|
export { startDev } from './dev.js';
|
|
10
11
|
export { runCoreSteps } from '../steps/run_core_steps.js';
|
|
@@ -19,7 +20,7 @@ export default async function buildSite(flags = {}) {
|
|
|
19
20
|
const errorParams = { errorMonitor, mode, logs, debug, testOpts };
|
|
20
21
|
const systemLog = getSystemLogger(logs, debug, systemLogFile);
|
|
21
22
|
try {
|
|
22
|
-
const { pluginsOptions, netlifyConfig: netlifyConfigA, siteInfo, userNodeVersion, stepsCount, timers, durationNs, configMutations, } = await execBuild({
|
|
23
|
+
const { pluginsOptions, netlifyConfig: netlifyConfigA, siteInfo, userNodeVersion, stepsCount, timers, durationNs, configMutations, metrics, } = await execBuild({
|
|
23
24
|
...flagsA,
|
|
24
25
|
buildId,
|
|
25
26
|
systemLogFile,
|
|
@@ -41,6 +42,7 @@ export default async function buildSite(flags = {}) {
|
|
|
41
42
|
durationNs,
|
|
42
43
|
statsdOpts,
|
|
43
44
|
systemLog,
|
|
45
|
+
metrics,
|
|
44
46
|
});
|
|
45
47
|
const { success, severityCode, status } = getSeverity('success');
|
|
46
48
|
await telemetryReport({
|
|
@@ -80,13 +82,14 @@ export default async function buildSite(flags = {}) {
|
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
// Logs and reports that a build successfully ended
|
|
83
|
-
const handleBuildSuccess = async function ({ framework, dry, logs, timers, durationNs, statsdOpts, systemLog }) {
|
|
85
|
+
const handleBuildSuccess = async function ({ framework, dry, logs, timers, durationNs, statsdOpts, systemLog, metrics, }) {
|
|
84
86
|
if (dry) {
|
|
85
87
|
return;
|
|
86
88
|
}
|
|
87
89
|
logBuildSuccess(logs);
|
|
88
90
|
logTimer(logs, durationNs, 'Netlify Build', systemLog);
|
|
89
91
|
await reportTimers(timers, statsdOpts, framework);
|
|
92
|
+
await reportMetrics(statsdOpts, metrics);
|
|
90
93
|
};
|
|
91
94
|
// Handles the calls and errors of telemetry reports
|
|
92
95
|
const telemetryReport = async function ({ deployId, buildId, status, stepsCount, pluginsOptions, durationNs, siteInfo, telemetry, userNodeVersion, framework, testOpts, errorParams, }) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { closeClient, formatTags, startClient, validateStatsDOptions } from '../report/statsd.js';
|
|
2
|
+
/**
|
|
3
|
+
* Record number of functions build and differentiate between autogenerated and user generated.
|
|
4
|
+
* Sends to statsd daemon.
|
|
5
|
+
*/
|
|
6
|
+
export const reportMetrics = async function (statsdOpts, metrics) {
|
|
7
|
+
if (!validateStatsDOptions(statsdOpts) || metrics.length === 0) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const client = await startClient(statsdOpts);
|
|
11
|
+
metrics.forEach((metric) => {
|
|
12
|
+
if (typeof client[metric.type] === 'function') {
|
|
13
|
+
client[metric.type](metric.name, metric.value, formatTags(metric.tags));
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
await closeClient(client);
|
|
17
|
+
};
|
|
@@ -43,14 +43,30 @@ const coreStep = async function ({ buildDir, constants: { EDGE_FUNCTIONS_DIST: d
|
|
|
43
43
|
systemLogger: featureFlags.edge_functions_system_logger ? systemLog : undefined,
|
|
44
44
|
internalSrcFolder: internalSrcPath,
|
|
45
45
|
});
|
|
46
|
+
const metrics = getMetrics(manifest);
|
|
46
47
|
systemLog('Edge Functions manifest:', manifest);
|
|
48
|
+
await validateEdgeFunctionsManifest(manifest, featureFlags);
|
|
49
|
+
return { metrics };
|
|
47
50
|
}
|
|
48
51
|
catch (error) {
|
|
49
52
|
tagBundlingError(error);
|
|
50
53
|
throw error;
|
|
51
54
|
}
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
};
|
|
56
|
+
const getMetrics = (manifest) => {
|
|
57
|
+
const numGenEfs = Object.values(manifest.function_config).filter((config) => config.generator).length;
|
|
58
|
+
const allRoutes = [...manifest.routes, ...manifest.post_cache_routes];
|
|
59
|
+
const totalEfs = [];
|
|
60
|
+
allRoutes.forEach((route) => {
|
|
61
|
+
if (!totalEfs.some((func) => func === route.function)) {
|
|
62
|
+
totalEfs.push(route.function);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const numUserEfs = totalEfs.length - numGenEfs;
|
|
66
|
+
return [
|
|
67
|
+
{ type: 'increment', name: 'buildbot.build.functions', value: numGenEfs, tags: { type: 'edge:generated' } },
|
|
68
|
+
{ type: 'increment', name: 'buildbot.build.functions', value: numUserEfs, tags: { type: 'edge:user' } },
|
|
69
|
+
];
|
|
54
70
|
};
|
|
55
71
|
// We run this core step if at least one of the functions directories (the
|
|
56
72
|
// one configured by the user or the internal one) exists. We use a dynamic
|
package/lib/plugins_core/edge_functions/validate_manifest/validate_edge_functions_manifest.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import { promises as fs } from 'fs';
|
|
2
|
-
import { join, resolve } from 'path';
|
|
3
1
|
import { ManifestValidationError, validateManifest } from '@netlify/edge-bundler';
|
|
4
2
|
import { tagBundlingError } from '../lib/error.js';
|
|
5
|
-
export const validateEdgeFunctionsManifest = async function (
|
|
6
|
-
const edgeFunctionsDistPath = resolve(buildDir, distDirectory);
|
|
7
|
-
const manifestPath = join(edgeFunctionsDistPath, 'manifest.json');
|
|
8
|
-
const data = await fs.readFile(manifestPath);
|
|
9
|
-
// @ts-expect-error TypeScript is not aware that parse can handle Buffer
|
|
10
|
-
const manifestData = JSON.parse(data);
|
|
3
|
+
export const validateEdgeFunctionsManifest = async function (manifest, featureFlags) {
|
|
11
4
|
try {
|
|
12
|
-
validateManifest(
|
|
5
|
+
validateManifest(manifest, featureFlags);
|
|
13
6
|
}
|
|
14
7
|
catch (error) {
|
|
15
8
|
if (error instanceof ManifestValidationError) {
|
|
@@ -79,10 +79,12 @@ const coreStep = async function ({ childEnv, constants: { INTERNAL_FUNCTIONS_SRC
|
|
|
79
79
|
logs,
|
|
80
80
|
repositoryRoot,
|
|
81
81
|
});
|
|
82
|
+
const metrics = getMetrics(internalFunctions, userFunctions);
|
|
82
83
|
return {
|
|
83
84
|
tags: {
|
|
84
85
|
bundler: bundlers,
|
|
85
86
|
},
|
|
87
|
+
metrics,
|
|
86
88
|
};
|
|
87
89
|
};
|
|
88
90
|
// We run this core step if at least one of the functions directories (the
|
|
@@ -115,3 +117,19 @@ export const zipItAndShipIt = {
|
|
|
115
117
|
return await zipFunctions(...args);
|
|
116
118
|
},
|
|
117
119
|
};
|
|
120
|
+
const getMetrics = (internalFunctions, userFunctions) => {
|
|
121
|
+
return [
|
|
122
|
+
{
|
|
123
|
+
type: 'increment',
|
|
124
|
+
name: 'buildbot.build.functions',
|
|
125
|
+
value: internalFunctions.length,
|
|
126
|
+
tags: { type: 'lambda:generated' },
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
type: 'increment',
|
|
130
|
+
name: 'buildbot.build.functions',
|
|
131
|
+
value: userFunctions.length,
|
|
132
|
+
tags: { type: 'lambda:user' },
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
};
|
package/lib/steps/core_step.js
CHANGED
|
@@ -6,7 +6,7 @@ export const fireCoreStep = async function ({ coreStep, coreStepId, coreStepName
|
|
|
6
6
|
try {
|
|
7
7
|
const configSideFiles = await listConfigSideFiles([headersPath, redirectsPath]);
|
|
8
8
|
const childEnvA = setEnvChanges(envChanges, { ...childEnv });
|
|
9
|
-
const { newEnvChanges = {}, configMutations: newConfigMutations = [], tags, } = await coreStep({
|
|
9
|
+
const { newEnvChanges = {}, configMutations: newConfigMutations = [], tags, metrics, } = await coreStep({
|
|
10
10
|
configPath,
|
|
11
11
|
outputConfigPath,
|
|
12
12
|
buildDir,
|
|
@@ -47,6 +47,7 @@ export const fireCoreStep = async function ({ coreStep, coreStepId, coreStepName
|
|
|
47
47
|
headersPath: headersPathA,
|
|
48
48
|
redirectsPath: redirectsPathA,
|
|
49
49
|
tags,
|
|
50
|
+
metrics,
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
catch (newError) {
|
package/lib/steps/return.js
CHANGED
|
@@ -2,7 +2,7 @@ import { logTimer } from '../log/messages/core.js';
|
|
|
2
2
|
import { logStepSuccess } from '../log/messages/steps.js';
|
|
3
3
|
import { handleStepError } from './error.js';
|
|
4
4
|
// Retrieve the return value of a step
|
|
5
|
-
export const getStepReturn = function ({ event, packageName, newError, newEnvChanges, newStatus, coreStep, coreStepName: timerName = `${packageName} ${event}`, childEnv, mode, api, errorMonitor, deployId, netlifyConfig, configMutations, headersPath, redirectsPath, logs, debug, timers, durationNs, testOpts, systemLog, quiet, }) {
|
|
5
|
+
export const getStepReturn = function ({ event, packageName, newError, newEnvChanges, newStatus, coreStep, coreStepName: timerName = `${packageName} ${event}`, childEnv, mode, api, errorMonitor, deployId, netlifyConfig, configMutations, headersPath, redirectsPath, logs, debug, timers, durationNs, testOpts, systemLog, quiet, metrics, }) {
|
|
6
6
|
if (newError !== undefined) {
|
|
7
7
|
return handleStepError({
|
|
8
8
|
event,
|
|
@@ -23,5 +23,5 @@ export const getStepReturn = function ({ event, packageName, newError, newEnvCha
|
|
|
23
23
|
logStepSuccess(logs);
|
|
24
24
|
logTimer(logs, durationNs, timerName, systemLog);
|
|
25
25
|
}
|
|
26
|
-
return { newEnvChanges, netlifyConfig, configMutations, headersPath, redirectsPath, newStatus, timers };
|
|
26
|
+
return { newEnvChanges, netlifyConfig, configMutations, headersPath, redirectsPath, newStatus, timers, metrics };
|
|
27
27
|
};
|
package/lib/steps/run_step.js
CHANGED
|
@@ -27,7 +27,7 @@ export const runStep = async function ({ event, childProcess, packageName, coreS
|
|
|
27
27
|
logStepStart({ logs, event, packageName, coreStepDescription, index, error, netlifyConfig });
|
|
28
28
|
}
|
|
29
29
|
const fireStep = getFireStep(packageName, coreStepId, event);
|
|
30
|
-
const { newEnvChanges, netlifyConfig: netlifyConfigA = netlifyConfig, configMutations: configMutationsA = configMutations, headersPath: headersPathA = headersPath, redirectsPath: redirectsPathA = redirectsPath, newError, newStatus, timers: timersA, durationNs, } = await fireStep({
|
|
30
|
+
const { newEnvChanges, netlifyConfig: netlifyConfigA = netlifyConfig, configMutations: configMutationsA = configMutations, headersPath: headersPathA = headersPath, redirectsPath: redirectsPathA = redirectsPath, newError, newStatus, timers: timersA, durationNs, metrics, } = await fireStep({
|
|
31
31
|
event,
|
|
32
32
|
childProcess,
|
|
33
33
|
packageName,
|
|
@@ -89,6 +89,7 @@ export const runStep = async function ({ event, childProcess, packageName, coreS
|
|
|
89
89
|
testOpts,
|
|
90
90
|
systemLog,
|
|
91
91
|
quiet,
|
|
92
|
+
metrics,
|
|
92
93
|
});
|
|
93
94
|
return { ...newValues, newIndex: index + 1 };
|
|
94
95
|
};
|
package/lib/steps/run_steps.js
CHANGED
|
@@ -8,8 +8,8 @@ import { runStep } from './run_step.js';
|
|
|
8
8
|
// If an error arises, runs `onError` events.
|
|
9
9
|
// Runs `onEnd` events at the end, whether an error was thrown or not.
|
|
10
10
|
export const runSteps = async function ({ steps, buildbotServerSocket, events, configPath, outputConfigPath, headersPath, redirectsPath, buildDir, repositoryRoot, nodePath, childEnv, context, branch, constants, mode, api, errorMonitor, deployId, errorParams, netlifyConfig, configOpts, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, }) {
|
|
11
|
-
const { index: stepsCount, error: errorA, netlifyConfig: netlifyConfigC, statuses: statusesB, failedPlugins: failedPluginsA, timers: timersC, configMutations: configMutationsB, } = await pReduce(steps, async ({ index, error, failedPlugins, envChanges, netlifyConfig: netlifyConfigA, configMutations, headersPath: headersPathA, redirectsPath: redirectsPathA, statuses, timers: timersA, }, { event, childProcess, packageName, coreStep, coreStepId, coreStepName, coreStepDescription, pluginPackageJson, loadedFrom, origin, condition, }) => {
|
|
12
|
-
const { newIndex = index, newError = error, failedPlugin = [], newEnvChanges = {}, netlifyConfig: netlifyConfigB = netlifyConfigA, configMutations: configMutationsA = configMutations, headersPath: headersPathB = headersPathA, redirectsPath: redirectsPathB = redirectsPathA, newStatus, timers: timersB = timersA, } = await runStep({
|
|
11
|
+
const { index: stepsCount, error: errorA, netlifyConfig: netlifyConfigC, statuses: statusesB, failedPlugins: failedPluginsA, timers: timersC, configMutations: configMutationsB, metrics: metricsC, } = await pReduce(steps, async ({ index, error, failedPlugins, envChanges, netlifyConfig: netlifyConfigA, configMutations, headersPath: headersPathA, redirectsPath: redirectsPathA, statuses, timers: timersA, metrics: metricsA, }, { event, childProcess, packageName, coreStep, coreStepId, coreStepName, coreStepDescription, pluginPackageJson, loadedFrom, origin, condition, }) => {
|
|
12
|
+
const { newIndex = index, newError = error, failedPlugin = [], newEnvChanges = {}, netlifyConfig: netlifyConfigB = netlifyConfigA, configMutations: configMutationsA = configMutations, headersPath: headersPathB = headersPathA, redirectsPath: redirectsPathB = redirectsPathA, newStatus, timers: timersB = timersA, metrics: metricsB = [], } = await runStep({
|
|
13
13
|
event,
|
|
14
14
|
childProcess,
|
|
15
15
|
packageName,
|
|
@@ -69,6 +69,7 @@ export const runSteps = async function ({ steps, buildbotServerSocket, events, c
|
|
|
69
69
|
redirectsPath: redirectsPathB,
|
|
70
70
|
statuses: statusesA,
|
|
71
71
|
timers: timersB,
|
|
72
|
+
metrics: [...metricsA, ...metricsB],
|
|
72
73
|
};
|
|
73
74
|
}, {
|
|
74
75
|
index: 0,
|
|
@@ -80,6 +81,7 @@ export const runSteps = async function ({ steps, buildbotServerSocket, events, c
|
|
|
80
81
|
redirectsPath,
|
|
81
82
|
statuses: [],
|
|
82
83
|
timers,
|
|
84
|
+
metrics: [],
|
|
83
85
|
});
|
|
84
86
|
// Instead of throwing any build failure right away, we wait for `onError`,
|
|
85
87
|
// etc. to complete. This is why we are throwing only now.
|
|
@@ -94,5 +96,6 @@ export const runSteps = async function ({ steps, buildbotServerSocket, events, c
|
|
|
94
96
|
failedPlugins: failedPluginsA,
|
|
95
97
|
timers: timersC,
|
|
96
98
|
configMutations: configMutationsB,
|
|
99
|
+
metrics: metricsC,
|
|
97
100
|
};
|
|
98
101
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/build",
|
|
3
|
-
"version": "29.
|
|
3
|
+
"version": "29.9.0",
|
|
4
4
|
"description": "Netlify build module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./lib/core/main.js",
|
|
@@ -147,5 +147,5 @@
|
|
|
147
147
|
"module": "commonjs"
|
|
148
148
|
}
|
|
149
149
|
},
|
|
150
|
-
"gitHead": "
|
|
150
|
+
"gitHead": "4eb3c1043739007bce796309e87b3db42d09a5f2"
|
|
151
151
|
}
|