Skip to content
This repository has been archived by the owner on Mar 23, 2022. It is now read-only.

Commit

Permalink
Updated GA data to use the database as cache #37
Browse files Browse the repository at this point in the history
Signed-off-by: RaenonX <[email protected]>
  • Loading branch information
RaenonX committed Jan 18, 2022
1 parent 394a863 commit e482a70
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 54 deletions.
2 changes: 1 addition & 1 deletion src/endpoints/info/homepage/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const handleHomepageLanding = async ({
payload,
mongoClient,
}: HandlerParams<HomepageLandingPayload>): Promise<HomepageLandingResponse> => {
const gaData = await getGaData();
const gaData = await getGaData(mongoClient);

const data: HomepageData = {
posts: {
Expand Down
36 changes: 12 additions & 24 deletions src/thirdparty/ga/controller.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {periodicActiveData, periodicCountryData, periodicLangData} from '../../../test/data/thirdparty/ga';
import {Application, createApp} from '../../app';
import {CACHE_LIFE_SECS} from '../../utils/cache/const';
import {getGaData, resetGaData} from './controller';
import {getGaData} from './controller';
import * as periodicActive from './data/periodicActive';
import * as periodicCountry from './data/periodicCountry';
import * as periodicTotal from './data/periodicTotal';
import {resetCache} from './dbCache';


describe('Google Analytics data cache', () => {
Expand All @@ -18,6 +18,8 @@ describe('Google Analytics data cache', () => {
});

beforeEach(async () => {
await app.reset();

fnFetchTotal = jest.spyOn(periodicTotal, 'getPeriodicLanguageUser')
.mockResolvedValue(periodicLangData);
fnFetchCountry = jest.spyOn(periodicCountry, 'getPeriodicCountryUser')
Expand All @@ -26,45 +28,37 @@ describe('Google Analytics data cache', () => {
.mockResolvedValue(periodicActiveData);
});

afterEach(() => {
resetGaData();
});

afterAll(async () => {
await app.close();
});

it('fetches data on initial request', async () => {
await getGaData();
await getGaData(app.mongoClient);
expect(fnFetchTotal).toHaveBeenCalledTimes(1);
expect(fnFetchCountry).toHaveBeenCalledTimes(1);
expect(fnFetchActive).toHaveBeenCalledTimes(1);
});

it('does not re-fetch data twice', async () => {
await getGaData();
await getGaData(app.mongoClient);
fnFetchTotal.mockClear();
fnFetchCountry.mockClear();
fnFetchActive.mockClear();
await getGaData();
await getGaData(app.mongoClient);
expect(fnFetchTotal).not.toHaveBeenCalled();
expect(fnFetchCountry).not.toHaveBeenCalled();
expect(fnFetchActive).not.toHaveBeenCalled();
});

it('re-fetches after the cache expires', async () => {
await getGaData();
await getGaData(app.mongoClient);
expect(fnFetchTotal).toHaveBeenCalledTimes(1);
expect(fnFetchCountry).toHaveBeenCalledTimes(1);
expect(fnFetchActive).toHaveBeenCalledTimes(1);

// Accelerate time
const now = Date.now();
jest
.spyOn(Date, 'now')
.mockImplementation(() => now + CACHE_LIFE_SECS * 1000 + 100000);
await resetCache(app.mongoClient);

await getGaData();
await getGaData(app.mongoClient);
expect(fnFetchTotal).toHaveBeenCalledTimes(2);
expect(fnFetchCountry).toHaveBeenCalledTimes(2);
expect(fnFetchActive).toHaveBeenCalledTimes(2);
Expand All @@ -73,18 +67,12 @@ describe('Google Analytics data cache', () => {
});

it('does not re-fetch before the cache expires', async () => {
await getGaData();
await getGaData(app.mongoClient);
expect(fnFetchTotal).toHaveBeenCalledTimes(1);
expect(fnFetchCountry).toHaveBeenCalledTimes(1);
expect(fnFetchActive).toHaveBeenCalledTimes(1);

// Accelerate time
const now = Date.now();
jest
.spyOn(Date, 'now')
.mockImplementation(() => now + CACHE_LIFE_SECS * 1000 / 2);

await getGaData();
await getGaData(app.mongoClient);
expect(fnFetchTotal).toHaveBeenCalledTimes(1);
expect(fnFetchCountry).toHaveBeenCalledTimes(1);
expect(fnFetchActive).toHaveBeenCalledTimes(1);
Expand Down
39 changes: 10 additions & 29 deletions src/thirdparty/ga/controller.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,21 @@
import env from 'env-var';
import {MongoClient} from 'mongodb';

import {periodicActiveData, periodicCountryData, periodicLangData} from '../../../test/data/thirdparty/ga';
import {isCi} from '../../api-def/utils';
import {isCacheExpired} from '../../utils/cache/func';
import {getPeriodicActiveUser} from './data/periodicActive';
import {getPeriodicCountryUser} from './data/periodicCountry';
import {getPeriodicLanguageUser} from './data/periodicTotal';
import {getCache, setCache} from './dbCache';
import {GACache} from './type';


const generateNewCache = (): GACache => ({
data: {
perCountry: {
D1: {countries: [], total: 0},
D7: {countries: [], total: 0},
D30: {countries: [], total: 0},
},
perLang: {
data: [],
toppedLang: [],
},
active: {
data: [],
},
},
lastFetchedEpoch: 0,
});

let cache: GACache = generateNewCache();

export const resetGaData = (): void => {
cache = generateNewCache();
};

export const getGaData = async (): Promise<GACache> => {
export const getGaData = async (mongoClient: MongoClient): Promise<GACache> => {
const currentEpoch = Math.round(Date.now() / 1000);

if (!isCacheExpired(cache, currentEpoch)) {
const cache = await getCache(mongoClient);

if (cache) {
return cache;
} else if (!isCi() && env.get('GA_DEV').asBool()) {
return {
Expand All @@ -49,7 +28,7 @@ export const getGaData = async (): Promise<GACache> => {
};
}

cache = {
const newCache: GACache = {
data: {
perCountry: await getPeriodicCountryUser(6),
perLang: await getPeriodicLanguageUser(30, 3),
Expand All @@ -58,5 +37,7 @@ export const getGaData = async (): Promise<GACache> => {
lastFetchedEpoch: currentEpoch,
};

return cache;
await setCache(mongoClient, newCache);

return newCache;
};
56 changes: 56 additions & 0 deletions src/thirdparty/ga/dbCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {Collection, MongoClient} from 'mongodb';

import {CollectionInfo} from '../../base/controller/info';
import {CACHE_LIFE_SECS} from '../../utils/cache/const';
import {getCollection} from '../../utils/mongodb';
import {GACache} from './type';


const dbInfo: CollectionInfo = {
dbName: 'cache',
collectionName: 'ga',
};

export enum GACacheKey {
data = 'd',
generationTimestamp = 'g'
}

export type GACacheDocument = {
[GACacheKey.data]: GACache,
[GACacheKey.generationTimestamp]: Date,
};

const getCacheCollection = (mongoClient: MongoClient): Collection<GACacheDocument> => {
return getCollection<GACacheDocument>(mongoClient, dbInfo, (collection) => {
// Enable data auto-expiration
collection.createIndex(GACacheKey.generationTimestamp, {expireAfterSeconds: CACHE_LIFE_SECS});
});
};

export const getCache = async (mongoClient: MongoClient): Promise<GACache | null> => {
const collection = getCacheCollection(mongoClient);

const cacheEntry = await collection.findOne();

if (!cacheEntry) {
return null;
}

return cacheEntry[GACacheKey.data];
};

export const setCache = async (mongoClient: MongoClient, data: GACache): Promise<void> => {
const collection = getCacheCollection(mongoClient);

await collection.insertOne({
[GACacheKey.data]: data,
[GACacheKey.generationTimestamp]: new Date(),
});
};

export const resetCache = async (mongoClient: MongoClient): Promise<void> => {
const collection = getCacheCollection(mongoClient);

await collection.deleteMany({});
};

0 comments on commit e482a70

Please sign in to comment.