mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
feat: Adds last-seen dat on toggles
When an application updates metrics for a toggle we now stores the timestamp on the toggle when it was last seen used by an application. This will make it much easier to detect toggles not in use anymore. closes #642
This commit is contained in:
parent
82b0a579e3
commit
cdfba8f7b1
@ -1497,6 +1497,9 @@ components:
|
|||||||
createdAt:
|
createdAt:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
lastSeenAt:
|
||||||
|
type: string
|
||||||
|
minLength: 1
|
||||||
x-tags:
|
x-tags:
|
||||||
- Responses
|
- Responses
|
||||||
200-events:
|
200-events:
|
||||||
|
@ -45,6 +45,7 @@ Used by db-migrate module to keep track of migrations.
|
|||||||
| archived | int4 | 10 | 1 | 0 | |
|
| archived | int4 | 10 | 1 | 0 | |
|
||||||
| strategies | json | 2147483647 | 1 | (null) | |
|
| strategies | json | 2147483647 | 1 | (null) | |
|
||||||
| type | varchar | 2147483647 | 1 | release | |
|
| type | varchar | 2147483647 | 1 | release | |
|
||||||
|
| last_seen_at | timestamp | 29 | 1 | (null) | |
|
||||||
|
|
||||||
## Table: _client_strategies_
|
## Table: _client_strategies_
|
||||||
|
|
||||||
|
@ -22,13 +22,14 @@ const FEATURE_COLUMNS = [
|
|||||||
'strategies',
|
'strategies',
|
||||||
'variants',
|
'variants',
|
||||||
'created_at',
|
'created_at',
|
||||||
|
'last_seen_at',
|
||||||
];
|
];
|
||||||
const TABLE = 'features';
|
const TABLE = 'features';
|
||||||
|
|
||||||
class FeatureToggleStore {
|
class FeatureToggleStore {
|
||||||
constructor(db, eventStore, eventBus, getLogger) {
|
constructor(db, eventStore, eventBus, getLogger) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.getLogger = getLogger('feature-toggle-store.js');
|
this.logger = getLogger('feature-toggle-store.js');
|
||||||
|
|
||||||
this.timer = action =>
|
this.timer = action =>
|
||||||
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
||||||
@ -115,6 +116,17 @@ class FeatureToggleStore {
|
|||||||
return rows.map(this.rowToFeature);
|
return rows.map(this.rowToFeature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async lastSeenToggles(togleNames) {
|
||||||
|
const now = new Date();
|
||||||
|
try {
|
||||||
|
await this.db(TABLE)
|
||||||
|
.whereIn('name', togleNames)
|
||||||
|
.update({ last_seen_at: now });
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error('Could not update lastSeen, error: ', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rowToFeature(row) {
|
rowToFeature(row) {
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new NotFoundError('No feature toggle found');
|
throw new NotFoundError('No feature toggle found');
|
||||||
@ -129,6 +141,7 @@ class FeatureToggleStore {
|
|||||||
strategies: row.strategies,
|
strategies: row.strategies,
|
||||||
variants: row.variants,
|
variants: row.variants,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
|
lastSeenAt: row.last_seen_at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ const TABLE = 'feature_types';
|
|||||||
class FeatureToggleStore {
|
class FeatureToggleStore {
|
||||||
constructor(db, getLogger) {
|
constructor(db, getLogger) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.getLogger = getLogger('feature-type-store.js');
|
this.logger = getLogger('feature-type-store.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll() {
|
async getAll() {
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const Controller = require('../controller');
|
const Controller = require('../controller');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -159,12 +157,12 @@ class FeatureController extends Controller {
|
|||||||
const feature = await this.featureToggleStore.getFeature(
|
const feature = await this.featureToggleStore.getFeature(
|
||||||
featureName,
|
featureName,
|
||||||
);
|
);
|
||||||
|
|
||||||
feature[field] = value;
|
feature[field] = value;
|
||||||
|
const validFeature = await featureShema.validateAsync(feature);
|
||||||
await this.eventStore.store({
|
await this.eventStore.store({
|
||||||
type: FEATURE_UPDATED,
|
type: FEATURE_UPDATED,
|
||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
data: feature,
|
data: validFeature,
|
||||||
});
|
});
|
||||||
res.json(feature).end();
|
res.json(feature).end();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,23 +1,19 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const Controller = require('../controller');
|
const Controller = require('../controller');
|
||||||
const ClientMetrics = require('../../client-metrics');
|
|
||||||
const schema = require('./metrics-schema');
|
const schema = require('./metrics-schema');
|
||||||
const { UPDATE_APPLICATION } = require('../../permissions');
|
const { UPDATE_APPLICATION } = require('../../permissions');
|
||||||
|
|
||||||
class MetricsController extends Controller {
|
class MetricsController extends Controller {
|
||||||
constructor(config) {
|
constructor(config, { clientMetricsService }) {
|
||||||
super(config);
|
super(config);
|
||||||
this.logger = config.getLogger('/admin-api/metrics.js');
|
this.logger = config.getLogger('/admin-api/metrics.js');
|
||||||
const {
|
const {
|
||||||
clientMetricsStore,
|
|
||||||
clientInstanceStore,
|
clientInstanceStore,
|
||||||
clientApplicationsStore,
|
clientApplicationsStore,
|
||||||
strategyStore,
|
strategyStore,
|
||||||
featureToggleStore,
|
featureToggleStore,
|
||||||
} = config.stores;
|
} = config.stores;
|
||||||
|
|
||||||
this.metrics = new ClientMetrics(clientMetricsStore);
|
this.metrics = clientMetricsService;
|
||||||
this.clientInstanceStore = clientInstanceStore;
|
this.clientInstanceStore = clientInstanceStore;
|
||||||
this.clientApplicationsStore = clientApplicationsStore;
|
this.clientApplicationsStore = clientApplicationsStore;
|
||||||
this.strategyStore = strategyStore;
|
this.strategyStore = strategyStore;
|
||||||
|
@ -8,20 +8,22 @@ const permissions = require('../../../test/fixtures/permissions');
|
|||||||
const getLogger = require('../../../test/fixtures/no-logger');
|
const getLogger = require('../../../test/fixtures/no-logger');
|
||||||
const getApp = require('../../app');
|
const getApp = require('../../app');
|
||||||
const { UPDATE_APPLICATION } = require('../../permissions');
|
const { UPDATE_APPLICATION } = require('../../permissions');
|
||||||
|
const { createServices } = require('../../services');
|
||||||
|
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
function getSetup() {
|
function getSetup() {
|
||||||
const stores = store.createStores();
|
const stores = store.createStores();
|
||||||
const perms = permissions();
|
const perms = permissions();
|
||||||
const app = getApp({
|
const config = {
|
||||||
baseUriPath: '',
|
baseUriPath: '',
|
||||||
stores,
|
|
||||||
eventBus,
|
eventBus,
|
||||||
extendedPermissions: true,
|
extendedPermissions: true,
|
||||||
preRouterHook: perms.hook,
|
preRouterHook: perms.hook,
|
||||||
getLogger,
|
getLogger,
|
||||||
});
|
};
|
||||||
|
const services = createServices(stores, config);
|
||||||
|
const app = getApp({ ...config, stores }, services);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
request: supertest(app),
|
request: supertest(app),
|
||||||
|
@ -4,11 +4,15 @@ const Controller = require('../controller');
|
|||||||
const { clientMetricsSchema } = require('./metrics-schema');
|
const { clientMetricsSchema } = require('./metrics-schema');
|
||||||
|
|
||||||
class ClientMetricsController extends Controller {
|
class ClientMetricsController extends Controller {
|
||||||
constructor({ clientMetricsStore, clientInstanceStore }, getLogger) {
|
constructor(
|
||||||
|
{ clientMetricsStore, clientInstanceStore, featureToggleStore },
|
||||||
|
getLogger,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
this.logger = getLogger('/api/client/metrics');
|
this.logger = getLogger('/api/client/metrics');
|
||||||
this.clientMetricsStore = clientMetricsStore;
|
this.clientMetricsStore = clientMetricsStore;
|
||||||
this.clientInstanceStore = clientInstanceStore;
|
this.clientInstanceStore = clientInstanceStore;
|
||||||
|
this.featureToggleStore = featureToggleStore;
|
||||||
|
|
||||||
this.post('/', this.registerMetrics);
|
this.post('/', this.registerMetrics);
|
||||||
}
|
}
|
||||||
@ -25,6 +29,8 @@ class ClientMetricsController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const toggleNames = Object.keys(value.bucket.toggles);
|
||||||
|
await this.featureToggleStore.lastSeenToggles(toggleNames);
|
||||||
await this.clientMetricsStore.insert(value);
|
await this.clientMetricsStore.insert(value);
|
||||||
await this.clientInstanceStore.insert({
|
await this.clientInstanceStore.insert({
|
||||||
appName: value.appName,
|
appName: value.appName,
|
||||||
|
@ -7,17 +7,20 @@ const store = require('../../../test/fixtures/store');
|
|||||||
const getLogger = require('../../../test/fixtures/no-logger');
|
const getLogger = require('../../../test/fixtures/no-logger');
|
||||||
const getApp = require('../../app');
|
const getApp = require('../../app');
|
||||||
const { clientMetricsSchema } = require('./metrics-schema');
|
const { clientMetricsSchema } = require('./metrics-schema');
|
||||||
|
const { createServices } = require('../../services');
|
||||||
|
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
function getSetup() {
|
function getSetup() {
|
||||||
const stores = store.createStores();
|
const stores = store.createStores();
|
||||||
const app = getApp({
|
const config = {
|
||||||
baseUriPath: '',
|
baseUriPath: '',
|
||||||
stores,
|
stores,
|
||||||
eventBus,
|
eventBus,
|
||||||
getLogger,
|
getLogger,
|
||||||
});
|
};
|
||||||
|
const services = createServices(stores, config);
|
||||||
|
const app = getApp(config, services);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
request: supertest(app),
|
request: supertest(app),
|
||||||
|
@ -38,7 +38,7 @@ const serverImpl = proxyquire('./server-impl', {
|
|||||||
return {
|
return {
|
||||||
db: { destroy: cb => cb() },
|
db: { destroy: cb => cb() },
|
||||||
clientInstanceStore: { destroy: noop },
|
clientInstanceStore: { destroy: noop },
|
||||||
clientMetricsStore: { destroy: noop },
|
clientMetricsStore: { destroy: noop, on: noop },
|
||||||
eventStore,
|
eventStore,
|
||||||
settingStore,
|
settingStore,
|
||||||
};
|
};
|
||||||
|
@ -11,8 +11,8 @@ const appName = 'appName';
|
|||||||
const instanceId = 'instanceId';
|
const instanceId = 'instanceId';
|
||||||
|
|
||||||
test('should work without state', t => {
|
test('should work without state', t => {
|
||||||
const store = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(store);
|
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||||
|
|
||||||
t.truthy(metrics.getAppsWithToggles());
|
t.truthy(metrics.getAppsWithToggles());
|
||||||
t.truthy(metrics.getTogglesMetrics());
|
t.truthy(metrics.getTogglesMetrics());
|
||||||
@ -23,8 +23,8 @@ test('should work without state', t => {
|
|||||||
test.cb('data should expire', t => {
|
test.cb('data should expire', t => {
|
||||||
const clock = lolex.install();
|
const clock = lolex.install();
|
||||||
|
|
||||||
const store = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(store);
|
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||||
|
|
||||||
metrics.addPayload({
|
metrics.addPayload({
|
||||||
appName,
|
appName,
|
||||||
@ -64,9 +64,9 @@ test.cb('data should expire', t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should listen to metrics from store', t => {
|
test('should listen to metrics from store', t => {
|
||||||
const store = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(store);
|
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||||
store.emit('metrics', {
|
clientMetricsStore.emit('metrics', {
|
||||||
appName,
|
appName,
|
||||||
instanceId,
|
instanceId,
|
||||||
bucket: {
|
bucket: {
|
||||||
@ -122,9 +122,9 @@ test('should listen to metrics from store', t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should build up list of seend toggles when new metrics arrives', t => {
|
test('should build up list of seend toggles when new metrics arrives', t => {
|
||||||
const store = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(store);
|
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||||
store.emit('metrics', {
|
clientMetricsStore.emit('metrics', {
|
||||||
appName,
|
appName,
|
||||||
instanceId,
|
instanceId,
|
||||||
bucket: {
|
bucket: {
|
||||||
@ -158,15 +158,15 @@ test('should build up list of seend toggles when new metrics arrives', t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should handle a lot of toggles', t => {
|
test('should handle a lot of toggles', t => {
|
||||||
const store = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(store);
|
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||||
|
|
||||||
const toggleCounts = {};
|
const toggleCounts = {};
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
toggleCounts[`toggle${i}`] = { yes: i, no: i };
|
toggleCounts[`toggle${i}`] = { yes: i, no: i };
|
||||||
}
|
}
|
||||||
|
|
||||||
store.emit('metrics', {
|
clientMetricsStore.emit('metrics', {
|
||||||
appName,
|
appName,
|
||||||
instanceId,
|
instanceId,
|
||||||
bucket: {
|
bucket: {
|
||||||
@ -185,8 +185,8 @@ test('should handle a lot of toggles', t => {
|
|||||||
test('should have correct values for lastMinute', t => {
|
test('should have correct values for lastMinute', t => {
|
||||||
const clock = lolex.install();
|
const clock = lolex.install();
|
||||||
|
|
||||||
const store = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(store);
|
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const input = [
|
const input = [
|
||||||
@ -228,7 +228,7 @@ test('should have correct values for lastMinute', t => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
input.forEach(bucket => {
|
input.forEach(bucket => {
|
||||||
store.emit('metrics', {
|
clientMetricsStore.emit('metrics', {
|
||||||
appName,
|
appName,
|
||||||
instanceId,
|
instanceId,
|
||||||
bucket,
|
bucket,
|
||||||
@ -257,8 +257,8 @@ test('should have correct values for lastMinute', t => {
|
|||||||
test('should have correct values for lastHour', t => {
|
test('should have correct values for lastHour', t => {
|
||||||
const clock = lolex.install();
|
const clock = lolex.install();
|
||||||
|
|
||||||
const store = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(store);
|
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const input = [
|
const input = [
|
||||||
@ -293,7 +293,7 @@ test('should have correct values for lastHour', t => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
input.forEach(bucket => {
|
input.forEach(bucket => {
|
||||||
store.emit('metrics', {
|
clientMetricsStore.emit('metrics', {
|
||||||
appName,
|
appName,
|
||||||
instanceId,
|
instanceId,
|
||||||
bucket,
|
bucket,
|
||||||
@ -337,9 +337,9 @@ test('should have correct values for lastHour', t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should not fail when toggle metrics is missing yes/no field', t => {
|
test('should not fail when toggle metrics is missing yes/no field', t => {
|
||||||
const store = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(store);
|
const metrics = new UnleashClientMetrics({ clientMetricsStore });
|
||||||
store.emit('metrics', {
|
clientMetricsStore.emit('metrics', {
|
||||||
appName,
|
appName,
|
||||||
instanceId,
|
instanceId,
|
||||||
bucket: {
|
bucket: {
|
@ -5,8 +5,8 @@
|
|||||||
const Projection = require('./projection.js');
|
const Projection = require('./projection.js');
|
||||||
const TTLList = require('./ttl-list.js');
|
const TTLList = require('./ttl-list.js');
|
||||||
|
|
||||||
module.exports = class UnleashClientMetrics {
|
module.exports = class ClientMetricsService {
|
||||||
constructor(clientMetricsStore) {
|
constructor({ clientMetricsStore }) {
|
||||||
this.globalCount = 0;
|
this.globalCount = 0;
|
||||||
this.apps = {};
|
this.apps = {};
|
||||||
|
|
@ -1,7 +1,9 @@
|
|||||||
const ProjectService = require('./project-service');
|
const ProjectService = require('./project-service');
|
||||||
const StateService = require('./state-service');
|
const StateService = require('./state-service');
|
||||||
|
const ClientMetricsService = require('./client-metrics');
|
||||||
|
|
||||||
module.exports.createServices = (stores, config) => ({
|
module.exports.createServices = (stores, config) => ({
|
||||||
projectService: new ProjectService(stores, config),
|
projectService: new ProjectService(stores, config),
|
||||||
stateService: new StateService(stores, config),
|
stateService: new StateService(stores, config),
|
||||||
|
clientMetricsService: new ClientMetricsService(stores, config),
|
||||||
});
|
});
|
||||||
|
10
migrations/20201216140726-add-last-seen-to-features.js
Normal file
10
migrations/20201216140726-add-last-seen-to-features.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql(
|
||||||
|
'ALTER TABLE features ADD "last_seen_at" TIMESTAMP WITH TIME ZONE;',
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, cb) {
|
||||||
|
return db.removeColumn('features', 'last_seen_at', cb);
|
||||||
|
};
|
@ -89,7 +89,7 @@
|
|||||||
"prom-client": "^12.0.0",
|
"prom-client": "^12.0.0",
|
||||||
"response-time": "^2.3.2",
|
"response-time": "^2.3.2",
|
||||||
"serve-favicon": "^2.5.0",
|
"serve-favicon": "^2.5.0",
|
||||||
"unleash-frontend": "3.8.2",
|
"unleash-frontend": "3.8.3",
|
||||||
"yargs": "^16.0.3"
|
"yargs": "^16.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -7,16 +7,12 @@ const supertest = require('supertest');
|
|||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const getApp = require('../../../lib/app');
|
const getApp = require('../../../lib/app');
|
||||||
const getLogger = require('../../fixtures/no-logger');
|
const getLogger = require('../../fixtures/no-logger');
|
||||||
const StateService = require('../../../lib/services/state-service');
|
const { createServices } = require('../../../lib/services');
|
||||||
|
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
function createApp(stores, adminAuthentication = 'none', preHook) {
|
function createApp(stores, adminAuthentication = 'none', preHook) {
|
||||||
const services = {
|
const config = {
|
||||||
stateService: new StateService(stores, { getLogger }),
|
|
||||||
};
|
|
||||||
return getApp(
|
|
||||||
{
|
|
||||||
stores,
|
stores,
|
||||||
eventBus,
|
eventBus,
|
||||||
preHook,
|
preHook,
|
||||||
@ -24,9 +20,10 @@ function createApp(stores, adminAuthentication = 'none', preHook) {
|
|||||||
secret: 'super-secret',
|
secret: 'super-secret',
|
||||||
sessionAge: 4000,
|
sessionAge: 4000,
|
||||||
getLogger,
|
getLogger,
|
||||||
},
|
};
|
||||||
services,
|
const services = createServices(stores, config);
|
||||||
);
|
// TODO: use create from server-impl instead?
|
||||||
|
return getApp(config, services);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
1
test/fixtures/fake-feature-toggle-store.js
vendored
1
test/fixtures/fake-feature-toggle-store.js
vendored
@ -26,5 +26,6 @@ module.exports = () => {
|
|||||||
addFeature: feature => _features.push(feature),
|
addFeature: feature => _features.push(feature),
|
||||||
getArchivedFeatures: () => Promise.resolve(_archive),
|
getArchivedFeatures: () => Promise.resolve(_archive),
|
||||||
addArchivedFeature: feature => _archive.push(feature),
|
addArchivedFeature: feature => _archive.push(feature),
|
||||||
|
lastSeenToggles: () => {},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -5715,10 +5715,10 @@ universalify@^0.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||||
|
|
||||||
unleash-frontend@3.8.2:
|
unleash-frontend@3.8.3:
|
||||||
version "3.8.2"
|
version "3.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-3.8.2.tgz#a33c7fa58b98071c77c06aa36a87c45fefc58c6e"
|
resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-3.8.3.tgz#18827b4f2a48af85ccc53a53c6ab05099326bcac"
|
||||||
integrity sha512-ieuDsF7WMZnnIaT4g9Df0oxvV37HxWdcx9QkBzK9ykQDkCIMYOideu82lXrZjfnI8oUbh/ZBYTRiE60+t7RC4A==
|
integrity sha512-Q+EfvwLzYkfecKp+FMnXKqWz8fLRboCugX54uN6VIWPh7FBdcasCwY4od494HobRC8lFoBxiLtuTkvSglxZ4EA==
|
||||||
|
|
||||||
unpipe@1.0.0, unpipe@~1.0.0:
|
unpipe@1.0.0, unpipe@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user