diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index fc821bff7875..705e417d4f50 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -55,6 +55,7 @@ "node-schedule": "^2.1.1", "pg": "^8.7.3", "proxy": "^2.1.1", + "redis-4": "npm:redis@^4.6.14", "reflect-metadata": "0.2.1", "rxjs": "^7.8.1", "yargs": "^16.2.0" diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.js b/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.js new file mode 100644 index 000000000000..31156674a654 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/scenario-redis-4.js @@ -0,0 +1,46 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, + integrations: [Sentry.redisIntegration({ cachePrefixes: ['redis-cache:'] })], +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +const { createClient } = require('redis-4'); + +async function run() { + const redisClient = await createClient().connect(); + + await Sentry.startSpan( + { + name: 'Test Span Redis 4', + op: 'test-span-redis-4', + }, + async () => { + try { + await redisClient.set('redis-test-key', 'test-value'); + await redisClient.set('redis-cache:test-key', 'test-value'); + + await redisClient.set('redis-cache:test-key-set-EX', 'test-value', { EX: 10 }); + await redisClient.setEx('redis-cache:test-key-setex', 10, 'test-value'); + + await redisClient.get('redis-test-key'); + await redisClient.get('redis-cache:test-key'); + await redisClient.get('redis-cache:unavailable-data'); + + await redisClient.mGet(['redis-test-key', 'redis-cache:test-key', 'redis-cache:unavailable-data']); + } finally { + await redisClient.disconnect(); + } + }, + ); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts index adbd88921a66..0c0807c8f480 100644 --- a/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/redis-cache/test.ts @@ -42,7 +42,7 @@ describe('redis cache auto instrumentation', () => { .start(done); }); - test('should create cache spans for prefixed keys', done => { + test('should create cache spans for prefixed keys (ioredis)', done => { const EXPECTED_TRANSACTION = { transaction: 'Test Span', spans: expect.arrayContaining([ @@ -139,4 +139,95 @@ describe('redis cache auto instrumentation', () => { .expect({ transaction: EXPECTED_TRANSACTION }) .start(done); }); + + test('should create cache spans for prefixed keys (redis-4)', done => { + const EXPECTED_REDIS_CONNECT = { + transaction: 'redis-connect', + }; + + const EXPECTED_TRANSACTION = { + transaction: 'Test Span Redis 4', + spans: expect.arrayContaining([ + // SET + expect.objectContaining({ + description: 'redis-cache:test-key', + op: 'cache.put', + origin: 'auto.db.otel.redis', + data: expect.objectContaining({ + 'sentry.origin': 'auto.db.otel.redis', + 'db.statement': 'SET redis-cache:test-key [1 other arguments]', + 'cache.key': ['redis-cache:test-key'], + 'cache.item_size': 2, + }), + }), + // SET (with EX) + expect.objectContaining({ + description: 'redis-cache:test-key-set-EX', + op: 'cache.put', + origin: 'auto.db.otel.redis', + data: expect.objectContaining({ + 'sentry.origin': 'auto.db.otel.redis', + 'db.statement': 'SET redis-cache:test-key-set-EX [3 other arguments]', + 'cache.key': ['redis-cache:test-key-set-EX'], + 'cache.item_size': 2, + }), + }), + // SETEX + expect.objectContaining({ + description: 'redis-cache:test-key-setex', + op: 'cache.put', + origin: 'auto.db.otel.redis', + data: expect.objectContaining({ + 'sentry.origin': 'auto.db.otel.redis', + 'db.statement': 'SETEX redis-cache:test-key-setex [2 other arguments]', + 'cache.key': ['redis-cache:test-key-setex'], + 'cache.item_size': 2, + }), + }), + // GET + expect.objectContaining({ + description: 'redis-cache:test-key', + op: 'cache.get', + origin: 'auto.db.otel.redis', + data: expect.objectContaining({ + 'sentry.origin': 'auto.db.otel.redis', + 'db.statement': 'GET redis-cache:test-key', + 'cache.hit': true, + 'cache.key': ['redis-cache:test-key'], + 'cache.item_size': 10, + }), + }), + // GET (unavailable - no cache hit) + expect.objectContaining({ + description: 'redis-cache:unavailable-data', + op: 'cache.get', + origin: 'auto.db.otel.redis', + data: expect.objectContaining({ + 'sentry.origin': 'auto.db.otel.redis', + 'db.statement': 'GET redis-cache:unavailable-data', + 'cache.hit': false, + 'cache.key': ['redis-cache:unavailable-data'], + }), + }), + // MGET + expect.objectContaining({ + description: 'redis-test-key, redis-cache:test-key, redis-cache:unavailable-data', + op: 'cache.get', + origin: 'auto.db.otel.redis', + data: expect.objectContaining({ + 'sentry.origin': 'auto.db.otel.redis', + 'db.statement': 'MGET [3 other arguments]', + 'cache.hit': true, + 'cache.key': ['redis-test-key', 'redis-cache:test-key', 'redis-cache:unavailable-data'], + }), + }), + ]), + }; + + createRunner(__dirname, 'scenario-redis-4.js') + .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port=6379'] }) + .expect({ transaction: EXPECTED_REDIS_CONNECT }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start(done); + }); }); diff --git a/packages/node/package.json b/packages/node/package.json index dcd3ef83f6a1..569211efbafe 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -88,6 +88,7 @@ "@opentelemetry/instrumentation-mysql2": "0.39.0", "@opentelemetry/instrumentation-nestjs-core": "0.38.0", "@opentelemetry/instrumentation-pg": "0.42.0", + "@opentelemetry/instrumentation-redis-4": "0.40.0", "@opentelemetry/resources": "^1.25.0", "@opentelemetry/sdk-trace-base": "^1.25.0", "@opentelemetry/semantic-conventions": "^1.25.0", diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index 55a01ba13651..bee4f06db8f5 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -13,7 +13,7 @@ import { instrumentMysql, mysqlIntegration } from './mysql'; import { instrumentMysql2, mysql2Integration } from './mysql2'; import { instrumentNest, nestIntegration } from './nest'; import { instrumentPostgres, postgresIntegration } from './postgres'; -import { redisIntegration } from './redis'; +import { instrumentRedis, redisIntegration } from './redis'; /** * With OTEL, all performance integrations will be added, as OTEL only initializes them when the patched package is actually required. @@ -60,5 +60,6 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => instrumentPostgres, instrumentHapi, instrumentGraphql, + instrumentRedis, ]; } diff --git a/packages/node/src/integrations/tracing/redis.ts b/packages/node/src/integrations/tracing/redis.ts index 87bcf6b9cb25..4204e4a2abe5 100644 --- a/packages/node/src/integrations/tracing/redis.ts +++ b/packages/node/src/integrations/tracing/redis.ts @@ -1,4 +1,7 @@ +import type { Span } from '@opentelemetry/api'; +import type { RedisResponseCustomAttributeFunction } from '@opentelemetry/instrumentation-ioredis'; import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis'; +import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis-4'; import { SEMANTIC_ATTRIBUTE_CACHE_HIT, SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE, @@ -9,12 +12,14 @@ import { spanToJSON, } from '@sentry/core'; import type { IntegrationFn } from '@sentry/types'; +import { truncate } from '@sentry/utils'; import { generateInstrumentOnce } from '../../otel/instrument'; import { GET_COMMANDS, calculateCacheItemSize, getCacheKeySafely, getCacheOperation, + isInCommands, shouldConsiderForCache, } from '../../utils/redisCache'; @@ -26,64 +31,80 @@ const INTEGRATION_NAME = 'Redis'; let _redisOptions: RedisOptions = {}; -export const instrumentRedis = generateInstrumentOnce(INTEGRATION_NAME, () => { +const cacheResponseHook: RedisResponseCustomAttributeFunction = (span: Span, redisCommand, cmdArgs, response) => { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.redis'); + + const safeKey = getCacheKeySafely(redisCommand, cmdArgs); + const cacheOperation = getCacheOperation(redisCommand); + + if ( + !safeKey || + !cacheOperation || + !_redisOptions?.cachePrefixes || + !shouldConsiderForCache(redisCommand, safeKey, _redisOptions.cachePrefixes) + ) { + // not relevant for cache + return; + } + + // otel/ioredis seems to be using the old standard, as there was a change to those params: https://github.com/open-telemetry/opentelemetry-specification/issues/3199 + // We are using params based on the docs: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/ + const networkPeerAddress = spanToJSON(span).data?.['net.peer.name']; + const networkPeerPort = spanToJSON(span).data?.['net.peer.port']; + if (networkPeerPort && networkPeerAddress) { + span.setAttributes({ 'network.peer.address': networkPeerAddress, 'network.peer.port': networkPeerPort }); + } + + const cacheItemSize = calculateCacheItemSize(response); + + if (cacheItemSize) { + span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE, cacheItemSize); + } + + if (isInCommands(GET_COMMANDS, redisCommand) && cacheItemSize !== undefined) { + span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, cacheItemSize > 0); + } + + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: cacheOperation, + [SEMANTIC_ATTRIBUTE_CACHE_KEY]: safeKey, + }); + + const spanDescription = safeKey.join(', '); + + span.updateName(truncate(spanDescription, 1024)); +}; + +const instrumentIORedis = generateInstrumentOnce('IORedis', () => { return new IORedisInstrumentation({ - responseHook: (span, redisCommand, cmdArgs, response) => { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.redis'); - - const safeKey = getCacheKeySafely(redisCommand, cmdArgs); - const cacheOperation = getCacheOperation(redisCommand); - - if ( - !safeKey || - !cacheOperation || - !_redisOptions?.cachePrefixes || - !shouldConsiderForCache(redisCommand, safeKey, _redisOptions.cachePrefixes) - ) { - // not relevant for cache - return; - } - - // otel/ioredis seems to be using the old standard, as there was a change to those params: https://github.com/open-telemetry/opentelemetry-specification/issues/3199 - // We are using params based on the docs: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/ - const networkPeerAddress = spanToJSON(span).data?.['net.peer.name']; - const networkPeerPort = spanToJSON(span).data?.['net.peer.port']; - if (networkPeerPort && networkPeerAddress) { - span.setAttributes({ 'network.peer.address': networkPeerAddress, 'network.peer.port': networkPeerPort }); - } - - const cacheItemSize = calculateCacheItemSize(response); - - if (cacheItemSize) { - span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE, cacheItemSize); - } - - if (GET_COMMANDS.includes(redisCommand) && cacheItemSize !== undefined) { - span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, cacheItemSize > 0); - } - - span.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: cacheOperation, - [SEMANTIC_ATTRIBUTE_CACHE_KEY]: safeKey, - }); - - const spanDescription = safeKey.join(', '); - - span.updateName(spanDescription.length > 1024 ? `${spanDescription.substring(0, 1024)}...` : spanDescription); - }, + responseHook: cacheResponseHook, }); }); +const instrumentRedis4 = generateInstrumentOnce('Redis-4', () => { + return new RedisInstrumentation({ + responseHook: cacheResponseHook, + }); +}); + +/** To be able to preload all Redis OTel instrumentations with just one ID ("Redis"), all the instrumentations are generated in this one function */ +export const instrumentRedis = Object.assign( + (): void => { + instrumentIORedis(); + instrumentRedis4(); + + // todo: implement them gradually + // new LegacyRedisInstrumentation({}), + }, + { id: INTEGRATION_NAME }, +); + const _redisIntegration = ((options: RedisOptions = {}) => { return { name: INTEGRATION_NAME, setupOnce() { _redisOptions = options; instrumentRedis(); - - // todo: implement them gradually - // new LegacyRedisInstrumentation({}), - // new RedisInstrumentation({}), }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/utils/redisCache.ts b/packages/node/src/utils/redisCache.ts index 9b394c6996da..9f0e7e6f1ac8 100644 --- a/packages/node/src/utils/redisCache.ts +++ b/packages/node/src/utils/redisCache.ts @@ -7,15 +7,19 @@ export const GET_COMMANDS = ['get', 'mget']; export const SET_COMMANDS = ['set', 'setex']; // todo: del, expire +/** Checks if a given command is in the list of redis commands. + * Useful because commands can come in lowercase or uppercase (depending on the library). */ +export function isInCommands(redisCommands: string[], command: string): boolean { + return redisCommands.includes(command.toLowerCase()); +} + /** Determine cache operation based on redis statement */ export function getCacheOperation( command: string, ): 'cache.get' | 'cache.put' | 'cache.remove' | 'cache.flush' | undefined { - const lowercaseStatement = command.toLowerCase(); - - if (GET_COMMANDS.includes(lowercaseStatement)) { + if (isInCommands(GET_COMMANDS, command)) { return 'cache.get'; - } else if (SET_COMMANDS.includes(lowercaseStatement)) { + } else if (isInCommands(SET_COMMANDS, command)) { return 'cache.put'; } else { return undefined; @@ -44,7 +48,7 @@ export function getCacheKeySafely(redisCommand: string, cmdArgs: IORedisCommandA } }; - if (SINGLE_ARG_COMMANDS.includes(redisCommand) && cmdArgs.length > 0) { + if (isInCommands(SINGLE_ARG_COMMANDS, redisCommand) && cmdArgs.length > 0) { return processArg(cmdArgs[0]); } diff --git a/packages/node/test/integrations/tracing/redis.test.ts b/packages/node/test/integrations/tracing/redis.test.ts index 307991f24a73..57eb727964be 100644 --- a/packages/node/test/integrations/tracing/redis.test.ts +++ b/packages/node/test/integrations/tracing/redis.test.ts @@ -13,12 +13,18 @@ describe('Redis', () => { expect(result).toBe(undefined); }); - it('should return a string representation of a single argument', () => { + it('should return a string array representation of a single argument', () => { const cmdArgs = ['key1']; const result = getCacheKeySafely('get', cmdArgs); expect(result).toStrictEqual(['key1']); }); + it('should return a string array representation of a single argument (uppercase)', () => { + const cmdArgs = ['key1']; + const result = getCacheKeySafely('GET', cmdArgs); + expect(result).toStrictEqual(['key1']); + }); + it('should return only the key for multiple arguments', () => { const cmdArgs = ['key1', 'the-value']; const result = getCacheKeySafely('get', cmdArgs); diff --git a/yarn.lock b/yarn.lock index 541965e3a543..3b13e04abf6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6486,6 +6486,15 @@ "@types/pg" "8.6.1" "@types/pg-pool" "2.0.4" +"@opentelemetry/instrumentation-redis-4@0.40.0": + version "0.40.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.40.0.tgz#4a1bc9bebfb869de8d982b1a1a5b550bdb68d15b" + integrity sha512-0ieQYJb6yl35kXA75LQUPhHtGjtQU9L85KlWa7d4ohBbk/iQKZ3X3CFl5jC5vNMq/GGPB3+w3IxNvALlHtrp7A== + dependencies: + "@opentelemetry/instrumentation" "^0.52.0" + "@opentelemetry/redis-common" "^0.36.2" + "@opentelemetry/semantic-conventions" "^1.22.0" + "@opentelemetry/instrumentation@0.52.0", "@opentelemetry/instrumentation@^0.52.0": version "0.52.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.0.tgz#f8b790bfb1c61c27e0ba846bc6d0e377da195d1e" @@ -6714,6 +6723,40 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@redis/bloom@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71" + integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg== + +"@redis/client@1.5.16": + version "1.5.16" + resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.16.tgz#1d5919077a06a4b935b0e4bef9e036eef1a10371" + integrity sha512-X1a3xQ5kEMvTib5fBrHKh6Y+pXbeKXqziYuxOUo1ojQNECg4M5Etd1qqyhMap+lFUOAh8S7UYevgJHOm4A+NOg== + dependencies: + cluster-key-slot "1.1.2" + generic-pool "3.9.0" + yallist "4.0.0" + +"@redis/graph@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.1.tgz#8c10df2df7f7d02741866751764031a957a170ea" + integrity sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw== + +"@redis/json@1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.6.tgz#b7a7725bbb907765d84c99d55eac3fcf772e180e" + integrity sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw== + +"@redis/search@1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.6.tgz#33bcdd791d9ed88ab6910243a355d85a7fedf756" + integrity sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw== + +"@redis/time-series@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.5.tgz#a6d70ef7a0e71e083ea09b967df0a0ed742bc6ad" + integrity sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg== + "@remix-run/node@^1.4.3": version "1.5.1" resolved "https://registry.yarnpkg.com/@remix-run/node/-/node-1.5.1.tgz#1c367d4035baaef8f0ea66962a826456d62f0030" @@ -12926,7 +12969,7 @@ clsx@^2.0.0: resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== -cluster-key-slot@^1.1.0: +cluster-key-slot@1.1.2, cluster-key-slot@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== @@ -17457,6 +17500,11 @@ generate-function@^2.3.1: dependencies: is-property "^1.0.2" +generic-pool@3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4" + integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -26456,6 +26504,18 @@ redeyed@~1.0.0: dependencies: esprima "~3.0.0" +"redis-4@npm:redis@^4.6.14": + version "4.6.14" + resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.14.tgz#599e49b65816c56a6683f6b19dc374c8e786d091" + integrity sha512-GrNg/e33HtsQwNXL7kJT+iNFPSwE1IPmd7wzV3j4f2z0EYxZfZE7FVTmUysgAtqQQtg5NXF5SNLR9OdO/UHOfw== + dependencies: + "@redis/bloom" "1.2.0" + "@redis/client" "1.5.16" + "@redis/graph" "1.1.1" + "@redis/json" "1.0.6" + "@redis/search" "1.1.6" + "@redis/time-series" "1.0.5" + redis-errors@^1.0.0, redis-errors@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" @@ -31487,16 +31547,16 @@ yalc@^1.0.0-pre.53: npm-packlist "^2.1.5" yargs "^16.1.1" +yallist@4.0.0, yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yallist@^3.0.0, yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - yam@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yam/-/yam-1.0.0.tgz#7f6c91dc0f5de75a031e6da6b3907c3d25ab0de5"