-
-
Notifications
You must be signed in to change notification settings - Fork 734
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: feature changes counted in new table (#4958)
As part of more telemetry on the usage of Unleash. This PR adds a new `stat_` prefixed table as well as a trigger on the events table trigger on each insert to increment a counter per environment per day. The trigger will trigger on every insert into the events base, but will filter and only increment the counter for events that actually have the environment set. (there are events, like user-created, that does not relate to a specific environment). Bit wary on this, but since we truncate down to row per (day, environment) combo, finding conflict and incrementing shouldn't take too long here. @ivarconr was it something like this you were considering?
- Loading branch information
Christopher Kolstad
authored
Oct 10, 2023
1 parent
fa4d6b2
commit 1edd73d
Showing
13 changed files
with
560 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
153 changes: 153 additions & 0 deletions
153
src/lib/features/instance-stats/getProductionChanges.e2e.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; | ||
import getLogger from '../../../test/fixtures/no-logger'; | ||
import { | ||
createGetProductionChanges, | ||
GetProductionChanges, | ||
} from './getProductionChanges'; | ||
import subDays from 'date-fns/subDays'; | ||
let db: ITestDb; | ||
let getProductionChanges: GetProductionChanges; | ||
|
||
const mockEventDaysAgo = (days: number, environment: string = 'production') => { | ||
const result = new Date(); | ||
result.setDate(result.getDate() - days); | ||
return { | ||
day: result, | ||
environment, | ||
updates: 1, | ||
}; | ||
}; | ||
|
||
const mockRawEventDaysAgo = ( | ||
days: number, | ||
environment: string = 'production', | ||
) => { | ||
const date = subDays(new Date(), days); | ||
return { | ||
type: 'FEATURE_UPDATED', | ||
created_at: date, | ||
created_by: 'testrunner', | ||
environment, | ||
feature_name: 'test.feature', | ||
announced: true, | ||
}; | ||
}; | ||
|
||
const noEnvironmentEvent = (days: number) => { | ||
return { | ||
type: 'FEATURE_UPDATED', | ||
created_by: 'testrunner', | ||
feature_name: 'test.feature', | ||
announced: true, | ||
}; | ||
}; | ||
|
||
beforeAll(async () => { | ||
db = await dbInit('product_changes_serial', getLogger); | ||
await db.rawDatabase('environments').insert({ | ||
name: 'production', | ||
type: 'production', | ||
enabled: true, | ||
protected: false, | ||
}); | ||
getProductionChanges = createGetProductionChanges(db.rawDatabase); | ||
}); | ||
|
||
afterEach(async () => { | ||
await db.rawDatabase('stat_environment_updates').truncate(); | ||
}); | ||
|
||
afterAll(async () => { | ||
await db.destroy(); | ||
}); | ||
|
||
test('should return 0 changes from an empty database', async () => { | ||
await expect(getProductionChanges()).resolves.toEqual({ | ||
last30: 0, | ||
last60: 0, | ||
last90: 0, | ||
}); | ||
}); | ||
|
||
test('should return 1 change', async () => { | ||
await db | ||
.rawDatabase('stat_environment_updates') | ||
.insert(mockEventDaysAgo(1)); | ||
|
||
await expect(getProductionChanges()).resolves.toEqual({ | ||
last30: 1, | ||
last60: 1, | ||
last90: 1, | ||
}); | ||
}); | ||
|
||
test('should handle intervals of activity', async () => { | ||
await db | ||
.rawDatabase('stat_environment_updates') | ||
.insert([ | ||
mockEventDaysAgo(5), | ||
mockEventDaysAgo(10), | ||
mockEventDaysAgo(20), | ||
mockEventDaysAgo(40), | ||
mockEventDaysAgo(70), | ||
mockEventDaysAgo(100), | ||
]); | ||
|
||
await expect(getProductionChanges()).resolves.toEqual({ | ||
last30: 3, | ||
last60: 4, | ||
last90: 5, | ||
}); | ||
}); | ||
|
||
test('an event being saved should add a count to the table', async () => { | ||
await db.rawDatabase | ||
.table('events') | ||
.insert(mockRawEventDaysAgo(70)) | ||
.returning('id'); | ||
|
||
await expect(getProductionChanges()).resolves.toEqual({ | ||
last30: 0, | ||
last60: 0, | ||
last90: 1, | ||
}); | ||
}); | ||
|
||
test('an event with no environment should not be counted', async () => { | ||
await db.rawDatabase('events').insert(noEnvironmentEvent(30)); | ||
await expect(getProductionChanges()).resolves.toEqual({ | ||
last30: 0, | ||
last60: 0, | ||
last90: 0, | ||
}); | ||
}); | ||
|
||
test('five events per day should be counted correctly', async () => { | ||
for (let i = 0; i < 100; i++) { | ||
for (let j: number = 0; j < 5; j++) { | ||
await db.rawDatabase.table('events').insert(mockRawEventDaysAgo(i)); | ||
} | ||
} | ||
await expect(getProductionChanges()).resolves.toEqual({ | ||
last30: 150, | ||
last60: 300, | ||
last90: 450, | ||
}); | ||
}); | ||
|
||
test('Events posted to a non production environment should not be included in count', async () => { | ||
await db.rawDatabase('environments').insert({ | ||
name: 'development', | ||
type: 'development', | ||
enabled: true, | ||
protected: false, | ||
}); | ||
await db.rawDatabase | ||
.table('events') | ||
.insert(mockRawEventDaysAgo(1, 'development')); | ||
await expect(getProductionChanges()).resolves.toEqual({ | ||
last30: 0, | ||
last60: 0, | ||
last90: 0, | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { type Db } from 'lib/server-impl'; | ||
import { GetActiveUsers } from './getActiveUsers'; | ||
|
||
export type GetProductionChanges = () => Promise<{ | ||
last30: number; | ||
last60: number; | ||
last90: number; | ||
}>; | ||
|
||
export const createGetProductionChanges = | ||
(db: Db): GetProductionChanges => | ||
async () => { | ||
const productionChanges = await db.raw(`SELECT SUM(CASE WHEN seu.day > NOW() - INTERVAL '30 days' THEN seu.updates END) AS last_month, | ||
SUM(CASE WHEN seu.day > NOW() - INTERVAL '60 days' THEN seu.updates END) AS last_two_months, | ||
SUM(CASE WHEN seu.day > NOW() - INTERVAL '90 days' THEN seu.updates END) AS last_quarter | ||
FROM stat_environment_updates seu | ||
LEFT JOIN environments e | ||
ON e.name = seu.environment | ||
WHERE e.type = 'production';`); | ||
return { | ||
last30: parseInt(productionChanges.rows[0]?.last_month || '0', 10), | ||
last60: parseInt( | ||
productionChanges.rows[0]?.last_two_months || '0', | ||
10, | ||
), | ||
last90: parseInt( | ||
productionChanges.rows[0]?.last_quarter || '0', | ||
10, | ||
), | ||
}; | ||
}; | ||
export const createFakeGetProductionChanges = | ||
( | ||
changesInProduction: Awaited<ReturnType<GetProductionChanges>> = { | ||
last30: 0, | ||
last60: 0, | ||
last90: 0, | ||
}, | ||
): GetProductionChanges => | ||
() => | ||
Promise.resolve(changesInProduction); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.