Skip to content

Commit 7c1987e

Browse files
feat: add caching for getting company information
Add local caching in a file when getting the company information. This allows us to minimize the number of calls to external services. As the companyInsightsService now uses multiple other services and is also exposed publically, rename it to companyInsightsController. Set the expiration of the cache to 2 days.
1 parent e8ce13b commit 7c1987e

9 files changed

+323
-215
lines changed

lib/bootstrap.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ $injector.require("userSettingsService", "./services/user-settings-service");
6767
$injector.requirePublic("analyticsSettingsService", "./services/analytics-settings-service");
6868
$injector.require("analyticsService", "./services/analytics/analytics-service");
6969
$injector.require("googleAnalyticsProvider", "./services/analytics/google-analytics-provider");
70-
$injector.requirePublicClass("companyInsightsService", "./services/company-insights-service");
70+
$injector.requirePublicClass("companyInsightsController", "./controllers/company-insights-controller");
7171

7272
$injector.require("platformCommandParameter", "./platform-command-param");
7373
$injector.requireCommand("create", "./commands/create-project");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { AnalyticsEventLabelDelimiter } from "../constants";
2+
import { cache } from "../common/decorators";
3+
import * as path from "path";
4+
5+
export class CompanyInsightsController implements ICompanyInsightsController {
6+
private static CACHE_TIMEOUT = 48 * 60 * 60 * 1000; // 2 days in milliseconds
7+
private get $jsonFileSettingsService(): IJsonFileSettingsService {
8+
return this.$injector.resolve<IJsonFileSettingsService>("jsonFileSettingsService", {
9+
jsonFileSettingsPath: path.join(this.$settingsService.getProfileDir(), "company-insights-data.json")
10+
});
11+
}
12+
13+
constructor(private $config: IConfiguration,
14+
private $httpClient: Server.IHttpClient,
15+
private $injector: IInjector,
16+
private $ipService: IIPService,
17+
private $logger: ILogger,
18+
private $settingsService: ISettingsService) { }
19+
20+
public async getCompanyData(): Promise<ICompanyData> {
21+
let companyData: ICompanyData = null;
22+
const { currentPublicIP, cacheKey } = await this.getIPInfo();
23+
24+
companyData = await this.getCompanyDataFromCache(cacheKey);
25+
26+
if (!companyData) {
27+
companyData = await this.getCompanyDataFromPlaygroundInsightsEndpoint();
28+
if (companyData && currentPublicIP) {
29+
await this.$jsonFileSettingsService.saveSetting<ICompanyData>(cacheKey, companyData, { useCaching: true });
30+
}
31+
}
32+
33+
return companyData;
34+
}
35+
36+
private async getIPInfo(): Promise<{ currentPublicIP: string, cacheKey: string }> {
37+
let currentPublicIP: string = null;
38+
let keyInJsonFile: string = null;
39+
40+
try {
41+
currentPublicIP = await this.$ipService.getCurrentIPv4Address();
42+
keyInJsonFile = `companyInformation_${currentPublicIP}`;
43+
} catch (err) {
44+
this.$logger.trace(`Unable to get current public ip address. Error is: `, err);
45+
}
46+
47+
return { currentPublicIP, cacheKey: keyInJsonFile };
48+
}
49+
50+
private async getCompanyDataFromCache(keyInJsonFile: string): Promise<ICompanyData> {
51+
let companyData: ICompanyData = null;
52+
53+
try {
54+
if (keyInJsonFile) {
55+
companyData = await this.$jsonFileSettingsService.getSettingValue<ICompanyData>(keyInJsonFile, { cacheTimeout: CompanyInsightsController.CACHE_TIMEOUT });
56+
}
57+
} catch (err) {
58+
this.$logger.trace(`Unable to get data from file, error is:`, err);
59+
}
60+
61+
return companyData;
62+
}
63+
64+
@cache()
65+
private async getCompanyDataFromPlaygroundInsightsEndpoint(): Promise<ICompanyData> {
66+
let companyData: ICompanyData = null;
67+
68+
try {
69+
const response = await this.$httpClient.httpRequest(this.$config.INSIGHTS_URL_ENDPOINT);
70+
const data = <IPlaygroundInsightsEndpointData>(JSON.parse(response.body));
71+
if (data.company) {
72+
const industries = _.isArray(data.company.industries) ? data.company.industries.join(AnalyticsEventLabelDelimiter) : null;
73+
companyData = {
74+
name: data.company.name,
75+
country: data.company.country,
76+
revenue: data.company.revenue,
77+
employeeCount: data.company.employeeCount,
78+
industries
79+
};
80+
}
81+
} catch (err) {
82+
this.$logger.trace(`Unable to get data for company. Error is: ${err}`);
83+
}
84+
85+
return companyData;
86+
}
87+
}
88+
89+
$injector.register("companyInsightsController", CompanyInsightsController);

lib/definitions/company-insights-service.d.ts renamed to lib/definitions/company-insights-controller.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ interface IPlaygroundInsightsEndpointData {
5050
/**
5151
* Describes the service that can be used to get insights about the company using the CLI.
5252
*/
53-
interface ICompanyInsightsService {
53+
interface ICompanyInsightsController {
5454
/**
5555
* Describes information about the company.
5656
* @returns {Promise<ICompanyData>}

lib/services/analytics/analytics-service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class AnalyticsService implements IAnalyticsService, IDisposable {
1515
private $options: IOptions,
1616
private $staticConfig: Config.IStaticConfig,
1717
private $prompter: IPrompter,
18-
private $userSettingsService: UserSettings.IUserSettingsService,
18+
private $userSettingsService: IUserSettingsService,
1919
private $analyticsSettingsService: IAnalyticsSettingsService,
2020
private $childProcess: IChildProcess,
2121
private $projectDataService: IProjectDataService,

lib/services/analytics/google-analytics-provider.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider {
1212
private $logger: ILogger,
1313
private $proxyService: IProxyService,
1414
private $config: IConfiguration,
15-
private $companyInsightsService: ICompanyInsightsService,
15+
private $companyInsightsController: ICompanyInsightsController,
1616
private analyticsLoggingService: IFileLogService) {
1717
}
1818

@@ -81,7 +81,7 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider {
8181
defaultValues[GoogleAnalyticsCustomDimensions.usedTutorial] = playgrounInfo.usedTutorial.toString();
8282
}
8383

84-
const companyData = await this.$companyInsightsService.getCompanyData();
84+
const companyData = await this.$companyInsightsController.getCompanyData();
8585
if (companyData) {
8686
defaultValues[GoogleAnalyticsCustomDimensions.companyName] = companyData.name;
8787
defaultValues[GoogleAnalyticsCustomDimensions.companyCountry] = companyData.country;

lib/services/company-insights-service.ts

-33
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { assert } from "chai";
2+
import { Yok } from "../../lib/common/yok";
3+
import { LoggerStub } from "../stubs";
4+
import { CompanyInsightsController } from "../../lib/controllers/company-insights-controller";
5+
6+
describe("companyInsightsController", () => {
7+
const insightsUrlEndpoint = "/api/insights";
8+
const currentIp = "8.8.8.8";
9+
const profileDir = "profileDir";
10+
const cacheTimeout = 48 * 60 * 60 * 1000; // 2 days in milliseconds
11+
const defaultCompanyData = {
12+
company: {
13+
name: "Progress",
14+
country: "Bulgaria",
15+
revenue: "123131",
16+
industries: [
17+
"Software",
18+
"Software 2"
19+
],
20+
employeeCount: "500"
21+
}
22+
};
23+
24+
const defaultExpectedCompanyData: ICompanyData = {
25+
name: "Progress",
26+
country: "Bulgaria",
27+
revenue: "123131",
28+
industries: "Software__Software 2",
29+
employeeCount: "500"
30+
};
31+
32+
const defaultExpectedDataPassedToGetSetting: any[] = [{ settingName: `companyInformation_${currentIp}`, cacheOpts: { cacheTimeout } }];
33+
const defaultExpectedDataPassedToSaveSetting: any[] = [
34+
{
35+
cacheOpts: {
36+
useCaching: true
37+
},
38+
key: "companyInformation_8.8.8.8",
39+
value: {
40+
country: "Bulgaria",
41+
employeeCount: "500",
42+
industries: "Software__Software 2",
43+
name: "Progress",
44+
revenue: "123131"
45+
}
46+
}
47+
];
48+
49+
let dataPassedToGetSettingValue: { settingName: string, cacheOpts?: ICacheTimeoutOpts }[] = [];
50+
let dataPassedToSaveSettingValue: { key: string, value: any, cacheOpts?: IUseCacheOpts }[] = [];
51+
let getSettingValueResult: IDictionary<any> = null;
52+
let httpRequestCounter = 0;
53+
let httpRequestResult: any = null;
54+
let testInjector: IInjector = null;
55+
let companyInsightsController: ICompanyInsightsController = null;
56+
57+
const createTestInjector = (): IInjector => {
58+
const injector = new Yok();
59+
injector.register("config", {
60+
INSIGHTS_URL_ENDPOINT: insightsUrlEndpoint
61+
});
62+
63+
injector.register("httpClient", {
64+
httpRequest: async (options: any, proxySettings?: IProxySettings): Promise<any> => {
65+
httpRequestCounter++;
66+
return { body: JSON.stringify(httpRequestResult) };
67+
}
68+
});
69+
70+
injector.register("logger", LoggerStub);
71+
injector.register("injector", injector);
72+
injector.register("ipService", {
73+
getCurrentIPv4Address: async (): Promise<string> => currentIp
74+
});
75+
76+
injector.register("settingsService", {
77+
getProfileDir: (): string => profileDir
78+
});
79+
80+
injector.register("jsonFileSettingsService", {
81+
getSettingValue: async (settingName: string, cacheOpts?: ICacheTimeoutOpts): Promise<any> => {
82+
dataPassedToGetSettingValue.push({ settingName, cacheOpts });
83+
return getSettingValueResult;
84+
},
85+
86+
saveSetting: async (key: string, value: any, cacheOpts?: IUseCacheOpts): Promise<void> => {
87+
dataPassedToSaveSettingValue.push({ key, value, cacheOpts });
88+
}
89+
});
90+
91+
injector.register("companyInsightsController", CompanyInsightsController);
92+
93+
return injector;
94+
};
95+
96+
beforeEach(() => {
97+
dataPassedToGetSettingValue = [];
98+
dataPassedToSaveSettingValue = [];
99+
getSettingValueResult = null;
100+
httpRequestCounter = 0;
101+
httpRequestResult = defaultCompanyData;
102+
testInjector = createTestInjector();
103+
companyInsightsController = testInjector.resolve<ICompanyInsightsController>("companyInsightsController");
104+
});
105+
106+
describe("getCompanyData", () => {
107+
describe("returns null when data does not exist in the cache and", () => {
108+
it("the http client fails to get data", async () => {
109+
const httpClient = testInjector.resolve<Server.IHttpClient>("httpClient");
110+
const errMsg = "custom error";
111+
httpClient.httpRequest = async () => {
112+
throw new Error(errMsg);
113+
};
114+
115+
const companyData = await companyInsightsController.getCompanyData();
116+
assert.isNull(companyData);
117+
const logger = testInjector.resolve<LoggerStub>("logger");
118+
assert.isTrue(logger.traceOutput.indexOf(errMsg) !== -1);
119+
});
120+
121+
it("the body of the response is not a valid JSON", async () => {
122+
const httpClient = testInjector.resolve<Server.IHttpClient>("httpClient");
123+
httpClient.httpRequest = async (): Promise<any> => {
124+
return { body: "invalid JSON" };
125+
};
126+
127+
const companyData = await companyInsightsController.getCompanyData();
128+
assert.isNull(companyData);
129+
const logger = testInjector.resolve<LoggerStub>("logger");
130+
assert.isTrue(logger.traceOutput.indexOf("SyntaxError: Unexpected token") !== -1);
131+
});
132+
133+
it("response does not contain company property", async () => {
134+
httpRequestResult = {
135+
foo: "bar"
136+
};
137+
138+
const companyData = await companyInsightsController.getCompanyData();
139+
assert.deepEqual(companyData, null);
140+
});
141+
});
142+
143+
describe("returns correct data when", () => {
144+
it("data for current ip exist in the cache", async () => {
145+
httpRequestResult = null;
146+
147+
getSettingValueResult = defaultExpectedCompanyData; // data in the file should be in the already parsed format
148+
const companyData = await companyInsightsController.getCompanyData();
149+
assert.deepEqual(companyData, defaultExpectedCompanyData);
150+
151+
assert.equal(httpRequestCounter, 0, "In case we have data for the company in our cache, we should not make any http requests");
152+
assert.deepEqual(dataPassedToGetSettingValue, defaultExpectedDataPassedToGetSetting);
153+
assert.deepEqual(dataPassedToSaveSettingValue, []);
154+
});
155+
156+
describe("data for current ip does not exist in the cache and", () => {
157+
158+
it("unable to get current ip address, but still can get company information", async () => {
159+
const ipService = testInjector.resolve<IIPService>("ipService");
160+
ipService.getCurrentIPv4Address = async (): Promise<string> => { throw new Error("Unable to get current ip addreess"); };
161+
162+
const companyData = await companyInsightsController.getCompanyData();
163+
assert.deepEqual(companyData, defaultExpectedCompanyData);
164+
assert.equal(httpRequestCounter, 1, "We should have only one http request");
165+
assert.deepEqual(dataPassedToGetSettingValue, [], "When we are unable to get IP, we should not try to get value from the cache.");
166+
assert.deepEqual(dataPassedToSaveSettingValue, [], "When we are unable to get IP, we should not persist anything.");
167+
});
168+
169+
it("response contains company property", async () => {
170+
const companyData = await companyInsightsController.getCompanyData();
171+
assert.deepEqual(companyData, defaultExpectedCompanyData);
172+
assert.deepEqual(dataPassedToGetSettingValue, defaultExpectedDataPassedToGetSetting);
173+
assert.deepEqual(dataPassedToSaveSettingValue, defaultExpectedDataPassedToSaveSetting);
174+
});
175+
176+
it("response contains company property and industries in it are not populated", async () => {
177+
httpRequestResult = {
178+
company: {
179+
name: "Progress",
180+
country: "Bulgaria",
181+
revenue: "123131",
182+
employeeCount: "500"
183+
}
184+
};
185+
186+
const companyData = await companyInsightsController.getCompanyData();
187+
assert.deepEqual(companyData, {
188+
name: "Progress",
189+
country: "Bulgaria",
190+
revenue: "123131",
191+
industries: null,
192+
employeeCount: "500"
193+
});
194+
195+
assert.deepEqual(dataPassedToGetSettingValue, defaultExpectedDataPassedToGetSetting);
196+
assert.deepEqual(dataPassedToSaveSettingValue, [
197+
{
198+
cacheOpts: {
199+
useCaching: true
200+
},
201+
key: "companyInformation_8.8.8.8",
202+
value: {
203+
country: "Bulgaria",
204+
employeeCount: "500",
205+
industries: null,
206+
name: "Progress",
207+
revenue: "123131"
208+
}
209+
}
210+
]);
211+
});
212+
213+
});
214+
});
215+
216+
it("is called only once per process", async () => {
217+
const companyData = await companyInsightsController.getCompanyData();
218+
assert.deepEqual(companyData, defaultExpectedCompanyData);
219+
assert.equal(httpRequestCounter, 1);
220+
221+
const companyDataSecondCall = await companyInsightsController.getCompanyData();
222+
assert.deepEqual(companyDataSecondCall, defaultExpectedCompanyData);
223+
assert.equal(httpRequestCounter, 1);
224+
});
225+
});
226+
});

0 commit comments

Comments
 (0)