Skip to content

Commit 400bf99

Browse files
authored
Add Debug class for errors (#1429)
* Add debug logging utils for language server and vs code * Add stack traces to debug errors * moved config loader to use debug util
1 parent 5053584 commit 400bf99

11 files changed

Lines changed: 314 additions & 71 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
- `apollo-graphql`
2020
- <First `apollo-graphql` related entry goes here>
2121
- `apollo-language-server`
22-
- <First `apollo-language-server` related entry goes here>
22+
- Add debugging util classes for better error/warning handling [#1429](https://github.com/apollographql/apollo-tooling/pull/1429)
2323
- `apollo-tools`
2424
- <First `apollo-tools` related entry goes here>
2525
- `vscode-apollo`
26-
- <First `vscode-apollo` related entry goes here>
26+
- Add debugging util class for better logging in vs code [#1429](https://github.com/apollographql/apollo-tooling/pull/1429)
2727

2828
## `apollo-language-server@1.14.3`
2929

packages/apollo-language-server/src/config/__tests__/loadConfig.ts

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,14 @@ describe("loadConfig", () => {
153153
writeFilesToDir(dir, {
154154
"package.json": `{"apollo":{"client": {"service": "hello"}} }`
155155
});
156+
157+
// silence the warning
158+
const spy = jest.spyOn(console, "warn");
159+
spy.mockImplementationOnce(() => {});
160+
156161
const config = await loadConfig({ configPath: dirPath });
157162

163+
spy.mockRestore();
158164
expect(config.client.service).toEqual("hello");
159165
});
160166

@@ -169,34 +175,47 @@ describe("loadConfig", () => {
169175
});
170176

171177
describe("errors", () => {
172-
it("throws when config file is empty", done => {
178+
it("throws when config file is empty", async () => {
173179
writeFilesToDir(dir, { "my.config.js": `` });
174180

175-
return loadConfig({
181+
const spy = jest.spyOn(console, "error");
182+
// use this to keep the log quiet
183+
spy.mockImplementation();
184+
185+
await loadConfig({
176186
configPath: dirPath,
177187
configFileName: "my.config.js"
178-
}).catch(err => {
179-
expect(err.message).toMatch(/.*A config file failed to load at.*/);
180-
done();
181188
});
189+
190+
expect(spy).toHaveBeenCalledWith(
191+
expect.stringMatching(/config file failed to load/i)
192+
);
193+
194+
spy.mockRestore();
182195
});
183196

184-
it("throws when explorer.search fails", done => {
197+
it("throws when explorer.search fails", async () => {
185198
writeFilesToDir(dir, { "my.config.js": `* 98375^%*&^ its lit` });
186199

187-
return loadConfig({
200+
const spy = jest.spyOn(console, "error");
201+
// use this to keep the log quiet
202+
spy.mockImplementation();
203+
204+
await loadConfig({
188205
configPath: dirPath,
189206
configFileName: "my.config.js"
190-
}).catch(err => {
191-
expect(err.message).toMatch(
192-
/.*A config file failed to load with options.*/
193-
);
194-
done();
195207
});
208+
209+
expect(spy).toHaveBeenCalledWith(
210+
expect.stringMatching(/config file failed to load/i)
211+
);
212+
213+
spy.mockRestore();
196214
});
197215

198216
it("issues a deprecation warning when loading config from package.json", async () => {
199-
jest.spyOn(global.console, "warn");
217+
const spy = jest.spyOn(console, "warn");
218+
spy.mockImplementation();
200219

201220
writeFilesToDir(dir, {
202221
"package.json": `{"apollo":{"client": {"service": "hello"}} }`
@@ -207,37 +226,47 @@ describe("loadConfig", () => {
207226
configFileName: "package.json"
208227
});
209228

210-
expect(console.warn.mock.calls[0][0]).toMatchInlineSnapshot(
211-
`"The \\"apollo\\" package.json configuration key will no longer be supported in Apollo v3. Please use the apollo.config.js file for Apollo project configuration. For more information, see: https://bit.ly/2ByILPj"`
229+
expect(spy).toHaveBeenCalledWith(
230+
expect.stringMatching(/The "apollo" package.json configuration/i)
212231
);
232+
233+
spy.mockRestore();
213234
});
214235

215-
it("throws if a config file was expected but not found", done => {
236+
it("throws if a config file was expected but not found", async () => {
237+
const spy = jest.spyOn(console, "error");
238+
spy.mockImplementation();
239+
216240
writeFilesToDir(dir, { "my.config.js": `module.exports = {}` });
217241

218-
return loadConfig({
242+
await loadConfig({
219243
configFileName: "my.TYPO.js",
220244
requireConfig: true // this is what we're testing
221-
}).catch(err => {
222-
expect(err.message).toMatch(/.*No Apollo config found for project*/);
223-
done();
224245
});
246+
247+
expect(spy).toHaveBeenCalledWith(
248+
expect.stringMatching(/no apollo config/i)
249+
);
250+
spy.mockRestore();
225251
});
226252

227-
it("throws if project type cant be resolved", () => {
253+
it("throws if project type cant be resolved", async () => {
254+
const spy = jest.spyOn(console, "error");
255+
spy.mockImplementation();
256+
228257
writeFilesToDir(dir, {
229258
"my.config.js": `module.exports = {}`
230259
});
231260

232-
const load = async () =>
233-
await loadConfig({
234-
configPath: dirPath,
235-
configFileName: "my.config.js"
236-
});
261+
await loadConfig({
262+
configPath: dirPath,
263+
configFileName: "my.config.js"
264+
});
237265

238-
return expect(load()).rejects.toMatchInlineSnapshot(
239-
`[Error: Unable to resolve project type. Please add either a client or service config. For more information, please refer to https://bit.ly/2ByILPj]`
266+
expect(spy).toHaveBeenCalledWith(
267+
expect.stringMatching(/unable to resolve/i)
240268
);
269+
spy.mockRestore();
241270
});
242271
});
243272

@@ -313,20 +342,6 @@ describe("loadConfig", () => {
313342

314343
expect(config.isService).toEqual(true);
315344
});
316-
317-
it("throws if project type cant be inferred", done => {
318-
writeFilesToDir(dir, {
319-
"my.config.js": `module.exports = { engine: { endpoint: 'http://a.a' } }`
320-
});
321-
322-
return loadConfig({
323-
configPath: dirPath,
324-
configFileName: "my.config.js"
325-
}).catch(err => {
326-
expect(err.message).toMatch(/.*Unable to resolve project type.*/);
327-
done();
328-
});
329-
});
330345
});
331346

332347
describe("service name", () => {

packages/apollo-language-server/src/config/loadConfig.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from "./config";
1515
import { getServiceFromKey } from "./utils";
1616
import URI from "vscode-uri";
17+
import { Debug } from "../utilities";
1718

1819
// config settings
1920
const MODULE_NAME = "apollo";
@@ -80,28 +81,26 @@ export async function loadConfig({
8081
ApolloConfigFormat
8182
>;
8283
} catch (error) {
83-
throw new Error(
84-
`A config file failed to load with options: ${JSON.stringify(
85-
arguments[0]
86-
)}.
87-
The error was: ${error}`
88-
);
84+
return Debug.error(`A config file failed to load with options: ${JSON.stringify(
85+
arguments[0]
86+
)}.
87+
The error was: ${error}`);
8988
}
9089

9190
if (configPath && !loadedConfig) {
92-
throw new Error(
91+
return Debug.error(
9392
`A config file failed to load at '${configPath}'. This is likely because this file is empty or malformed. For more information, please refer to: https://bit.ly/2ByILPj`
9493
);
9594
}
9695

9796
if (loadedConfig && loadedConfig.filepath.endsWith("package.json")) {
98-
console.warn(
97+
Debug.warning(
9998
'The "apollo" package.json configuration key will no longer be supported in Apollo v3. Please use the apollo.config.js file for Apollo project configuration. For more information, see: https://bit.ly/2ByILPj'
10099
);
101100
}
102101

103102
if (requireConfig && !loadedConfig) {
104-
throw new Error(
103+
return Debug.error(
105104
`No Apollo config found for project. For more information, please refer to:
106105
https://bit.ly/2ByILPj`
107106
);
@@ -135,7 +134,7 @@ export async function loadConfig({
135134
else if (loadedConfig && loadedConfig.config.client) projectType = "client";
136135
else if (loadedConfig && loadedConfig.config.service) projectType = "service";
137136
else
138-
throw new Error(
137+
return Debug.error(
139138
"Unable to resolve project type. Please add either a client or service config. For more information, please refer to https://bit.ly/2ByILPj"
140139
);
141140

packages/apollo-language-server/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ export * from "./config";
2424
// Generated types
2525
import * as graphqlTypes from "./graphqlTypes";
2626
export { graphqlTypes };
27+
28+
// debug logger
29+
export { Debug } from "./utilities";

packages/apollo-language-server/src/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import { QuickPickItem } from "vscode";
1212
import { GraphQLWorkspace } from "./workspace";
1313
import { GraphQLLanguageProvider } from "./languageProvider";
1414
import { LanguageServerLoadingHandler } from "./loadingHandler";
15-
import { debounceHandler } from "./utilities";
15+
import { debounceHandler, Debug } from "./utilities";
1616

1717
const connection = createConnection(ProposedFeatures.all);
18+
Debug.SetConnection(connection);
1819

1920
let hasWorkspaceFolderCapability = false;
2021

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { IConnection } from "vscode-languageserver";
2+
3+
/**
4+
* for errors (and other logs in debug mode) we want to print
5+
* a stack trace showing where they were thrown. This uses an
6+
* Error's stack trace, removes the three frames regarding
7+
* this file (since they're useless) and returns the rest of the trace.
8+
*/
9+
const createAndTrimStackTrace = () => {
10+
let stack: string | undefined = new Error().stack;
11+
// remove the lines in the stack from _this_ function and the caller (in this file) and shorten the trace
12+
return stack && stack.split("\n").length > 2
13+
? stack
14+
.split("\n")
15+
.slice(3, 7)
16+
.join("\n")
17+
: stack;
18+
};
19+
20+
type Logger = (message?: any) => void;
21+
22+
export class Debug {
23+
private static connection?: IConnection;
24+
private static infoLogger: Logger = message =>
25+
console.log("[INFO] " + message);
26+
private static warningLogger: Logger = message =>
27+
console.warn("[WARNING] " + message);
28+
private static errorLogger: Logger = message =>
29+
console.error("[ERROR] " + message);
30+
31+
/**
32+
* Setting a connection overrides the default info/warning/error
33+
* loggers to pass a notification to the connection
34+
*/
35+
public static SetConnection(conn: IConnection) {
36+
Debug.connection = conn;
37+
Debug.infoLogger = message =>
38+
Debug.connection!.sendNotification("serverDebugMessage", {
39+
type: "info",
40+
message: message
41+
});
42+
Debug.warningLogger = message =>
43+
Debug.connection!.sendNotification("serverDebugMessage", {
44+
type: "warning",
45+
message: message
46+
});
47+
Debug.errorLogger = message =>
48+
Debug.connection!.sendNotification("serverDebugMessage", {
49+
type: "error",
50+
message: message
51+
});
52+
}
53+
54+
/**
55+
* Allow callers to set their own error logging utils.
56+
* These will default to console.log/warn/error
57+
*/
58+
public static SetLoggers({
59+
info,
60+
warning,
61+
error
62+
}: {
63+
info?: Logger;
64+
warning?: Logger;
65+
error?: Logger;
66+
}) {
67+
if (info) Debug.infoLogger = info;
68+
if (warning) Debug.warningLogger = warning;
69+
if (error) Debug.errorLogger = error;
70+
}
71+
72+
public static info(message: string) {
73+
Debug.infoLogger(message);
74+
}
75+
76+
public static error(message: string) {
77+
const stack = createAndTrimStackTrace();
78+
Debug.errorLogger(`${message}\n${stack}`);
79+
}
80+
81+
public static warning(message: string) {
82+
Debug.warningLogger(message);
83+
}
84+
85+
public static sendErrorTelemetry(message: string) {
86+
Debug.connection &&
87+
Debug.connection.sendNotification("serverDebugMessage", {
88+
type: "errorTelemetry",
89+
message: message
90+
});
91+
}
92+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./debouncer";
22
export * from "./uri";
3+
export { Debug } from "./debug";

packages/apollo-language-server/src/workspace.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ServiceID, SchemaTag, ClientIdentity } from "./engine";
1818
import { GraphQLClientProject, isClientProject } from "./project/client";
1919
import { GraphQLServiceProject } from "./project/service";
2020
import URI from "vscode-uri";
21+
import { Debug } from "./utilities";
2122

2223
export interface WorkspaceConfig {
2324
clientIdentity?: ClientIdentity;
@@ -123,20 +124,26 @@ export class GraphQLWorkspace {
123124
const projectConfigs = Array.from(apolloConfigFolders).map(configFolder =>
124125
loadConfig({ configPath: configFolder, requireConfig: true })
125126
.then(config => {
126-
foundConfigs.push(config);
127-
const projectsForConfig = config.projects.map(projectConfig =>
128-
this.createProject({ config, folder })
129-
);
130-
131-
const existingProjects =
132-
this.projectsByFolderUri.get(folder.uri) || [];
133-
134-
this.projectsByFolderUri.set(folder.uri, [
135-
...existingProjects,
136-
...projectsForConfig
137-
]);
127+
if (config) {
128+
foundConfigs.push(config);
129+
const projectsForConfig = config.projects.map(projectConfig =>
130+
this.createProject({ config, folder })
131+
);
132+
133+
const existingProjects =
134+
this.projectsByFolderUri.get(folder.uri) || [];
135+
136+
this.projectsByFolderUri.set(folder.uri, [
137+
...existingProjects,
138+
...projectsForConfig
139+
]);
140+
} else {
141+
Debug.error(
142+
`Workspace failed to load config from: ${configFolder}/`
143+
);
144+
}
138145
})
139-
.catch(error => console.error(error))
146+
.catch(error => Debug.error(error))
140147
);
141148

142149
await Promise.all(projectConfigs);

0 commit comments

Comments
 (0)