diff --git a/.oxlintrc.base.json b/.oxlintrc.base.json index 8fe892e64ad9..71bc1b6a68ad 100644 --- a/.oxlintrc.base.json +++ b/.oxlintrc.base.json @@ -141,19 +141,12 @@ } }, { - "files": ["**/integrations/tracing/dataloader/vendored/**/*.ts"], - "rules": { - "typescript/no-explicit-any": "off" - } - }, - { - "files": ["**/integrations/tracing/genericPool/vendored/**/*.ts"], - "rules": { - "typescript/no-explicit-any": "off" - } - }, - { - "files": ["**/integrations/tracing/knex/vendored/**/*.ts"], + "files": [ + "**/integrations/tracing/dataloader/vendored/**/*.ts", + "**/integrations/tracing/genericPool/vendored/**/*.ts", + "**/integrations/fs/vendored/**/*.ts", + "**/integrations/tracing/knex/vendored/**/*.ts" + ], "rules": { "typescript/no-explicit-any": "off" } diff --git a/packages/node/package.json b/packages/node/package.json index c8126e098268..883fe311a050 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -70,7 +70,6 @@ "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/instrumentation-amqplib": "0.61.0", "@opentelemetry/instrumentation-connect": "0.57.0", - "@opentelemetry/instrumentation-fs": "0.33.0", "@opentelemetry/instrumentation-graphql": "0.62.0", "@opentelemetry/instrumentation-hapi": "0.60.0", "@opentelemetry/instrumentation-http": "0.214.0", diff --git a/packages/node/src/integrations/fs.ts b/packages/node/src/integrations/fs/index.ts similarity index 98% rename from packages/node/src/integrations/fs.ts rename to packages/node/src/integrations/fs/index.ts index 2fd05ad0a09d..05dd8ebaadb3 100644 --- a/packages/node/src/integrations/fs.ts +++ b/packages/node/src/integrations/fs/index.ts @@ -1,4 +1,4 @@ -import { FsInstrumentation } from '@opentelemetry/instrumentation-fs'; +import { FsInstrumentation } from './vendored/instrumentation'; import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { generateInstrumentOnce } from '@sentry/node-core'; diff --git a/packages/node/src/integrations/fs/vendored/constants.ts b/packages/node/src/integrations/fs/vendored/constants.ts new file mode 100644 index 000000000000..c454b761679d --- /dev/null +++ b/packages/node/src/integrations/fs/vendored/constants.ts @@ -0,0 +1,143 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-fs + * - Upstream version: @opentelemetry/instrumentation-fs@0.37.0 + */ +/* eslint-disable */ + +import type { FMember, FPMember } from './types'; + +export const PROMISE_FUNCTIONS: FPMember[] = [ + 'access', + 'appendFile', + 'chmod', + 'chown', + 'copyFile', + 'cp' as FPMember, // added in v16 + 'lchown', + 'link', + 'lstat', + 'lutimes', // added in v12 + 'mkdir', + 'mkdtemp', + 'open', + 'opendir', // added in v12 + 'readdir', + 'readFile', + 'readlink', + 'realpath', + 'rename', + 'rm', // added in v14 + 'rmdir', + 'stat', + 'symlink', + 'truncate', + 'unlink', + 'utimes', + 'writeFile', + // 'lchmod', // only implemented on macOS +]; + +export const CALLBACK_FUNCTIONS: FMember[] = [ + 'access', + 'appendFile', + 'chmod', + 'chown', + 'copyFile', + 'cp' as FMember, // added in v16 + 'exists', // deprecated, inconsistent cb signature, handling separately when patching + 'lchown', + 'link', + 'lstat', + 'lutimes', // added in v12 + 'mkdir', + 'mkdtemp', + 'open', + 'opendir', // added in v12 + 'readdir', + 'readFile', + 'readlink', + 'realpath', + 'realpath.native', + 'rename', + 'rm', // added in v14 + 'rmdir', + 'stat', + 'symlink', + 'truncate', + 'unlink', + 'utimes', + 'writeFile', + // 'close', // functions on file descriptor + // 'fchmod', // functions on file descriptor + // 'fchown', // functions on file descriptor + // 'fdatasync', // functions on file descriptor + // 'fstat', // functions on file descriptor + // 'fsync', // functions on file descriptor + // 'ftruncate', // functions on file descriptor + // 'futimes', // functions on file descriptor + // 'lchmod', // only implemented on macOS + // 'read', // functions on file descriptor + // 'readv', // functions on file descriptor + // 'write', // functions on file descriptor + // 'writev', // functions on file descriptor +]; + +export const SYNC_FUNCTIONS: FMember[] = [ + 'accessSync', + 'appendFileSync', + 'chmodSync', + 'chownSync', + 'copyFileSync', + 'cpSync' as FMember, // added in v16 + 'existsSync', + 'lchownSync', + 'linkSync', + 'lstatSync', + 'lutimesSync', // added in v12 + 'mkdirSync', + 'mkdtempSync', + 'opendirSync', // added in v12 + 'openSync', + 'readdirSync', + 'readFileSync', + 'readlinkSync', + 'realpathSync', + 'realpathSync.native', + 'renameSync', + 'rmdirSync', + 'rmSync', // added in v14 + 'statSync', + 'symlinkSync', + 'truncateSync', + 'unlinkSync', + 'utimesSync', + 'writeFileSync', + // 'closeSync', // functions on file descriptor + // 'fchmodSync', // functions on file descriptor + // 'fchownSync', // functions on file descriptor + // 'fdatasyncSync', // functions on file descriptor + // 'fstatSync', // functions on file descriptor + // 'fsyncSync', // functions on file descriptor + // 'ftruncateSync', // functions on file descriptor + // 'futimesSync', // functions on file descriptor + // 'lchmodSync', // only implemented on macOS + // 'readSync', // functions on file descriptor + // 'readvSync', // functions on file descriptor + // 'writeSync', // functions on file descriptor + // 'writevSync', // functions on file descriptor +]; diff --git a/packages/node/src/integrations/fs/vendored/instrumentation.ts b/packages/node/src/integrations/fs/vendored/instrumentation.ts new file mode 100644 index 000000000000..e48789f1a3f0 --- /dev/null +++ b/packages/node/src/integrations/fs/vendored/instrumentation.ts @@ -0,0 +1,394 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-fs + * - Upstream version: @opentelemetry/instrumentation-fs@0.37.0 + * - Minor TypeScript strictness adjustments for this repository's compiler settings + */ +/* eslint-disable */ + +import * as api from '@opentelemetry/api'; +import { isTracingSuppressed, suppressTracing } from '@opentelemetry/core'; +import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; +import { SDK_VERSION } from '@sentry/core'; +import { CALLBACK_FUNCTIONS, PROMISE_FUNCTIONS, SYNC_FUNCTIONS } from './constants'; +import type * as fs from 'fs'; +import type { FMember, FPMember, CreateHook, EndHook, FsInstrumentationConfig } from './types'; +import { promisify } from 'util'; +import { indexFs } from './utils'; + +type FS = typeof fs; +type FSPromises = (typeof fs)['promises']; + +const PACKAGE_NAME = '@sentry/instrumentation-fs'; + +/** + * This is important for 2-level functions like `realpath.native` to retain the 2nd-level + * when patching the 1st-level. + */ +function patchedFunctionWithOriginalProperties ReturnType>( + patchedFunction: T, + original: T, +): T { + return Object.assign(patchedFunction, original); +} + +export class FsInstrumentation extends InstrumentationBase { + constructor(config: FsInstrumentationConfig = {}) { + super(PACKAGE_NAME, SDK_VERSION, config); + } + + init(): (InstrumentationNodeModuleDefinition | InstrumentationNodeModuleDefinition)[] { + return [ + new InstrumentationNodeModuleDefinition( + 'fs', + ['*'], + (fs: FS) => { + for (const fName of SYNC_FUNCTIONS) { + const { objectToPatch, functionNameToPatch } = indexFs(fs, fName); + + if (isWrapped(objectToPatch[functionNameToPatch])) { + this._unwrap(objectToPatch, functionNameToPatch); + } + this._wrap(objectToPatch, functionNameToPatch, this._patchSyncFunction.bind(this, fName) as any); + } + for (const fName of CALLBACK_FUNCTIONS) { + const { objectToPatch, functionNameToPatch } = indexFs(fs, fName); + if (isWrapped(objectToPatch[functionNameToPatch])) { + this._unwrap(objectToPatch, functionNameToPatch); + } + if (fName === 'exists') { + // handling separately because of the inconsistent cb style: + // `exists` doesn't have error as the first argument, but the result + this._wrap( + objectToPatch, + functionNameToPatch, + this._patchExistsCallbackFunction.bind(this, fName) as any, + ); + continue; + } + this._wrap(objectToPatch, functionNameToPatch, this._patchCallbackFunction.bind(this, fName) as any); + } + for (const fName of PROMISE_FUNCTIONS) { + if (isWrapped(fs.promises[fName])) { + this._unwrap(fs.promises, fName); + } + this._wrap(fs.promises, fName, this._patchPromiseFunction.bind(this, fName) as any); + } + return fs; + }, + (fs: FS) => { + if (fs === undefined) return; + for (const fName of SYNC_FUNCTIONS) { + const { objectToPatch, functionNameToPatch } = indexFs(fs, fName); + if (isWrapped(objectToPatch[functionNameToPatch])) { + this._unwrap(objectToPatch, functionNameToPatch); + } + } + for (const fName of CALLBACK_FUNCTIONS) { + const { objectToPatch, functionNameToPatch } = indexFs(fs, fName); + if (isWrapped(objectToPatch[functionNameToPatch])) { + this._unwrap(objectToPatch, functionNameToPatch); + } + } + for (const fName of PROMISE_FUNCTIONS) { + if (isWrapped(fs.promises[fName])) { + this._unwrap(fs.promises, fName); + } + } + }, + ), + new InstrumentationNodeModuleDefinition( + 'fs/promises', + ['*'], + (fsPromises: FSPromises) => { + for (const fName of PROMISE_FUNCTIONS) { + if (isWrapped(fsPromises[fName])) { + this._unwrap(fsPromises, fName); + } + this._wrap(fsPromises, fName, this._patchPromiseFunction.bind(this, fName) as any); + } + return fsPromises; + }, + (fsPromises: FSPromises) => { + if (fsPromises === undefined) return; + for (const fName of PROMISE_FUNCTIONS) { + if (isWrapped(fsPromises[fName])) { + this._unwrap(fsPromises, fName); + } + } + }, + ), + ]; + } + + protected _patchSyncFunction ReturnType>(functionName: FMember, original: T): T { + const instrumentation = this; + const patchedFunction = function (this: any, ...args: any[]) { + const activeContext = api.context.active(); + + if (!instrumentation._shouldTrace(activeContext)) { + return original.apply(this, args); + } + if ( + instrumentation._runCreateHook(functionName, { + args: args, + }) === false + ) { + return api.context.with(suppressTracing(activeContext), original, this, ...args); + } + + const span = instrumentation.tracer.startSpan(`fs ${functionName}`) as api.Span; + + try { + // Suppress tracing for internal fs calls + const res = api.context.with(suppressTracing(api.trace.setSpan(activeContext, span)), original, this, ...args); + instrumentation._runEndHook(functionName, { args: args, span }); + return res; + } catch (error: any) { + span.recordException(error); + span.setStatus({ + message: error.message, + code: api.SpanStatusCode.ERROR, + }); + instrumentation._runEndHook(functionName, { args: args, span, error }); + throw error; + } finally { + span.end(); + } + }; + return patchedFunctionWithOriginalProperties(patchedFunction as any, original); + } + + protected _patchCallbackFunction ReturnType>(functionName: FMember, original: T): T { + const instrumentation = this; + const patchedFunction = function (this: any, ...args: any[]) { + const activeContext = api.context.active(); + + if (!instrumentation._shouldTrace(activeContext)) { + return original.apply(this, args); + } + if ( + instrumentation._runCreateHook(functionName, { + args: args, + }) === false + ) { + return api.context.with(suppressTracing(activeContext), original, this, ...args); + } + + const lastIdx = args.length - 1; + const cb = args[lastIdx]; + if (typeof cb === 'function') { + const span = instrumentation.tracer.startSpan(`fs ${functionName}`) as api.Span; + + // Return to the context active during the call in the callback + args[lastIdx] = api.context.bind(activeContext, function (this: unknown, error?: Error) { + if (error) { + span.recordException(error); + span.setStatus({ + message: error.message, + code: api.SpanStatusCode.ERROR, + }); + } + instrumentation._runEndHook(functionName, { + args: args, + span, + error, + }); + span.end(); + return cb.apply(this, arguments); + }); + + try { + // Suppress tracing for internal fs calls + return api.context.with(suppressTracing(api.trace.setSpan(activeContext, span)), original, this, ...args); + } catch (error: any) { + span.recordException(error); + span.setStatus({ + message: error.message, + code: api.SpanStatusCode.ERROR, + }); + instrumentation._runEndHook(functionName, { + args: args, + span, + error, + }); + span.end(); + throw error; + } + } else { + // TODO: what to do if we are pretty sure it's going to throw + return original.apply(this, args); + } + }; + return patchedFunctionWithOriginalProperties(patchedFunction as any, original); + } + + protected _patchExistsCallbackFunction ReturnType>( + functionName: 'exists', + original: T, + ): T { + const instrumentation = this; + const patchedFunction = function (this: any, ...args: any[]) { + const activeContext = api.context.active(); + + if (!instrumentation._shouldTrace(activeContext)) { + return original.apply(this, args); + } + if ( + instrumentation._runCreateHook(functionName, { + args: args, + }) === false + ) { + return api.context.with(suppressTracing(activeContext), original, this, ...args); + } + + const lastIdx = args.length - 1; + const cb = args[lastIdx]; + if (typeof cb === 'function') { + const span = instrumentation.tracer.startSpan(`fs ${functionName}`) as api.Span; + + // Return to the context active during the call in the callback + args[lastIdx] = api.context.bind(activeContext, function (this: unknown) { + // `exists` never calls the callback with an error + instrumentation._runEndHook(functionName, { + args: args, + span, + }); + span.end(); + return cb.apply(this, arguments); + }); + + try { + // Suppress tracing for internal fs calls + return api.context.with(suppressTracing(api.trace.setSpan(activeContext, span)), original, this, ...args); + } catch (error: any) { + span.recordException(error); + span.setStatus({ + message: error.message, + code: api.SpanStatusCode.ERROR, + }); + instrumentation._runEndHook(functionName, { + args: args, + span, + error, + }); + span.end(); + throw error; + } + } else { + return original.apply(this, args); + } + }; + const functionWithOriginalProperties = patchedFunctionWithOriginalProperties(patchedFunction, original); + + // `exists` has a custom promisify function because of the inconsistent signature + // replicating that on the patched function + const promisified = function (path: unknown) { + return new Promise(resolve => functionWithOriginalProperties(path, resolve)); + }; + Object.defineProperty(promisified, 'name', { value: functionName }); + Object.defineProperty(functionWithOriginalProperties, promisify.custom, { + value: promisified, + }); + + return functionWithOriginalProperties as T; + } + + protected _patchPromiseFunction ReturnType>(functionName: FPMember, original: T): T { + const instrumentation = this; + const patchedFunction = async function (this: any, ...args: any[]) { + const activeContext = api.context.active(); + + if (!instrumentation._shouldTrace(activeContext)) { + return original.apply(this, args); + } + if ( + instrumentation._runCreateHook(functionName, { + args: args, + }) === false + ) { + return api.context.with(suppressTracing(activeContext), original, this, ...args); + } + + const span = instrumentation.tracer.startSpan(`fs ${functionName}`) as api.Span; + + try { + // Suppress tracing for internal fs calls + const res = await api.context.with( + suppressTracing(api.trace.setSpan(activeContext, span)), + original, + this, + ...args, + ); + instrumentation._runEndHook(functionName, { args: args, span }); + return res; + } catch (error: any) { + span.recordException(error); + span.setStatus({ + message: error.message, + code: api.SpanStatusCode.ERROR, + }); + instrumentation._runEndHook(functionName, { args: args, span, error }); + throw error; + } finally { + span.end(); + } + }; + return patchedFunctionWithOriginalProperties(patchedFunction as any, original); + } + + protected _runCreateHook(...args: Parameters): ReturnType { + const { createHook } = this.getConfig(); + if (typeof createHook === 'function') { + try { + return createHook(...args); + } catch (e) { + this._diag.error('caught createHook error', e); + } + } + return true; + } + + protected _runEndHook(...args: Parameters): ReturnType { + const { endHook } = this.getConfig(); + if (typeof endHook === 'function') { + try { + endHook(...args); + } catch (e) { + this._diag.error('caught endHook error', e); + } + } + } + + protected _shouldTrace(context: api.Context): boolean { + if (isTracingSuppressed(context)) { + // Performance optimization. Avoid creating additional contexts and spans + // if we already know that the tracing is being suppressed. + return false; + } + + const { requireParentSpan } = this.getConfig(); + if (requireParentSpan) { + const parentSpan = api.trace.getSpan(context); + if (parentSpan == null) { + return false; + } + } + + return true; + } +} diff --git a/packages/node/src/integrations/fs/vendored/types.ts b/packages/node/src/integrations/fs/vendored/types.ts new file mode 100644 index 000000000000..38806d6a8435 --- /dev/null +++ b/packages/node/src/integrations/fs/vendored/types.ts @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-fs + * - Upstream version: @opentelemetry/instrumentation-fs@0.37.0 + */ +/* eslint-disable */ + +import type * as fs from 'fs'; + +import type * as api from '@opentelemetry/api'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +export type FunctionPropertyNames = Exclude< + { + [K in keyof T]: T[K] extends Function ? K : never; + }[keyof T], + undefined +>; +export type FunctionProperties = Pick>; + +export type FunctionPropertyNamesTwoLevels = Exclude< + { + [K in keyof T]: { + [L in keyof T[K]]: L extends string + ? T[K][L] extends Function + ? K extends string + ? L extends string + ? `${K}.${L}` + : never + : never + : never + : never; + }[keyof T[K]]; + }[keyof T], + undefined +>; + +export type Member = FunctionPropertyNames | FunctionPropertyNamesTwoLevels; +export type FMember = FunctionPropertyNames | FunctionPropertyNamesTwoLevels; +export type FPMember = + | FunctionPropertyNames<(typeof fs)['promises']> + | FunctionPropertyNamesTwoLevels<(typeof fs)['promises']>; + +export type CreateHook = (functionName: FMember | FPMember, info: { args: ArrayLike }) => boolean; +export type EndHook = ( + functionName: FMember | FPMember, + info: { args: ArrayLike; span: api.Span; error?: Error }, +) => void; + +export interface FsInstrumentationConfig extends InstrumentationConfig { + createHook?: CreateHook; + endHook?: EndHook; + requireParentSpan?: boolean; +} diff --git a/packages/node/src/integrations/fs/vendored/utils.ts b/packages/node/src/integrations/fs/vendored/utils.ts new file mode 100644 index 000000000000..3a76d0c17493 --- /dev/null +++ b/packages/node/src/integrations/fs/vendored/utils.ts @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-fs + * - Upstream version: @opentelemetry/instrumentation-fs@0.37.0 + */ +/* eslint-disable */ + +import type { FunctionPropertyNames, FMember } from './types'; +import type * as fs from 'fs'; +type FS = typeof fs; + +export function splitTwoLevels( + functionName: FMember, +): [FunctionPropertyNames & string] | [FunctionPropertyNames & string, string] { + const memberParts = functionName.split('.'); + if (memberParts.length > 1) { + if (memberParts.length !== 2) throw Error(`Invalid member function name ${functionName}`); + return memberParts as [FunctionPropertyNames & string, string]; + } else { + return [functionName as FunctionPropertyNames & string]; + } +} + +export function indexFs( + fs: FSObject, + member: FMember, +): { objectToPatch: any; functionNameToPatch: string } { + if (!member) throw new Error(JSON.stringify({ member })); + const splitResult = splitTwoLevels(member); + const [functionName1, functionName2] = splitResult; + if (functionName2) { + return { + objectToPatch: fs[functionName1], + functionNameToPatch: functionName2, + }; + } else { + return { + objectToPatch: fs, + functionNameToPatch: functionName1, + }; + } +} diff --git a/yarn.lock b/yarn.lock index f64f43e5535d..73a2c2820143 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6086,14 +6086,6 @@ "@opentelemetry/semantic-conventions" "^1.27.0" "@types/connect" "3.4.38" -"@opentelemetry/instrumentation-fs@0.33.0": - version "0.33.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.33.0.tgz#75f2ccf653b772801b398cc2ad0974e8785f2e3d" - integrity sha512-sCZWXGalQ01wr3tAhSR9ucqFJ0phidpAle6/17HVjD6gN8FLmZMK/8sKxdXYHy3PbnlV1P4zeiSVFNKpbFMNLA== - dependencies: - "@opentelemetry/core" "^2.0.0" - "@opentelemetry/instrumentation" "^0.214.0" - "@opentelemetry/instrumentation-graphql@0.62.0": version "0.62.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.62.0.tgz#dc2fc92c6be331c4f95b62a40983c8aedb8f9bf9"