Skip to content

Commit 352484b

Browse files
authored
node grpc spike dashboard to server (#18691)
* [public-api] add dummy service for testing * [public-api] proxy dummy to server * [public-api] hello service server impl * [server] fix API contribution bindings * [dashboard] emulate unary call * only if actually called * [dummy] auth * fix tests * [server] add interceptor to public api * add server side observability * fix port name * change to unimplemented for unknown methods * [public-api] client metrics * fix metrics imports * align server metrics * actually fix metrics * add feature flags * fix server side streams * [dashboard] hook error reporting * rebase and fix imports * feature flagged metrics from dashboard * revert GRPC_TYPE * address feedback
1 parent 8d4128b commit 352484b

File tree

33 files changed

+1891
-124
lines changed

33 files changed

+1891
-124
lines changed

components/dashboard/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"version": "0.0.0",
55
"private": true,
66
"dependencies": {
7-
"@bufbuild/connect-web": "^0.2.1",
7+
"@bufbuild/connect-web": "^0.13.0",
88
"@gitpod/gitpod-protocol": "0.1.5",
99
"@gitpod/public-api": "0.1.5",
1010
"@stripe/react-stripe-js": "^1.7.2",
@@ -14,9 +14,11 @@
1414
"@tanstack/react-query-devtools": "^4.29.19",
1515
"@tanstack/react-query-persist-client": "^4.29.19",
1616
"@types/react-datepicker": "^4.8.0",
17+
"buffer": "^4.3.0",
1718
"classnames": "^2.3.1",
1819
"configcat-js": "^6.0.0",
1920
"countries-list": "^2.6.1",
21+
"crypto-browserify": "3.12.0",
2022
"dayjs": "^1.11.5",
2123
"file-saver": "^2.0.5",
2224
"idb-keyval": "^6.2.0",
@@ -25,6 +27,7 @@
2527
"monaco-editor": "^0.25.2",
2628
"p-throttle": "^5.1.0",
2729
"pretty-bytes": "^6.1.0",
30+
"process": "^0.11.10",
2831
"query-string": "^7.1.1",
2932
"react": "^17.0.1",
3033
"react-confetti": "^6.1.0",
@@ -37,16 +40,13 @@
3740
"react-popper": "^2.3.0",
3841
"react-portal": "^4.2.2",
3942
"react-router-dom": "^5.2.0",
40-
"validator": "^13.9.0",
41-
"xterm": "^4.11.0",
42-
"xterm-addon-fit": "^0.5.0",
43-
"crypto-browserify": "3.12.0",
43+
"setimmediate": "^1.0.5",
4444
"stream-browserify": "^2.0.1",
4545
"url": "^0.11.1",
4646
"util": "^0.11.1",
47-
"buffer": "^4.3.0",
48-
"process": "^0.11.10",
49-
"setimmediate": "^1.0.5"
47+
"validator": "^13.9.0",
48+
"xterm": "^4.11.0",
49+
"xterm-addon-fit": "^0.5.0"
5050
},
5151
"devDependencies": {
5252
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",

components/dashboard/src/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7+
// this should stay at the top to enable monitoring as soon as possible
8+
import "./service/metrics";
9+
710
import "setimmediate"; // important!, required by vscode-jsonrpc
811
import dayjs from "dayjs";
912
import duration from "dayjs/plugin/duration";
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
8+
import { MetricsReporter } from "@gitpod/public-api/lib/metrics";
9+
import { getExperimentsClient } from "../experiments/client";
10+
11+
const originalConsoleError = console.error;
12+
13+
const options = {
14+
gitpodUrl: new GitpodHostUrl(window.location.href).withoutWorkspacePrefix().toString(),
15+
clientName: "dashboard",
16+
clientVersion: "",
17+
logError: originalConsoleError.bind(console),
18+
isEnabled: () => getExperimentsClient().getValueAsync("dashboard_metrics_enabled", false, {}),
19+
};
20+
fetch("/api/version").then(async (res) => {
21+
const version = await res.text();
22+
options.clientVersion = version;
23+
});
24+
const metricsReporter = new MetricsReporter(options);
25+
metricsReporter.startReporting();
26+
27+
window.addEventListener("unhandledrejection", (event) => {
28+
reportError("Unhandled promise rejection", event.reason);
29+
});
30+
window.addEventListener("error", (event) => {
31+
let message = "Unhandled error";
32+
if (event.message) {
33+
message += ": " + event.message;
34+
}
35+
reportError(message, event.error);
36+
});
37+
38+
console.error = function (...args) {
39+
originalConsoleError.apply(console, args);
40+
reportError(...args);
41+
};
42+
43+
function reportError(...args: any[]) {
44+
let err = undefined;
45+
let details = undefined;
46+
if (args[0] instanceof Error) {
47+
err = args[0];
48+
details = args[1];
49+
} else if (typeof args[0] === "string") {
50+
err = new Error(args[0]);
51+
if (args[1] instanceof Error) {
52+
err.message += ": " + args[1].message;
53+
err.name = args[1].name;
54+
err.stack = args[1].stack;
55+
details = args[2];
56+
} else if (typeof args[1] === "string") {
57+
err.message += ": " + args[1];
58+
details = args[2];
59+
} else {
60+
details = args[1];
61+
}
62+
}
63+
64+
let data = undefined;
65+
if (details && typeof details === "object") {
66+
data = Object.fromEntries(
67+
Object.entries(details)
68+
.filter(([key, value]) => {
69+
return (
70+
typeof value === "string" ||
71+
typeof value === "number" ||
72+
typeof value === "boolean" ||
73+
value === null ||
74+
typeof value === "undefined"
75+
);
76+
})
77+
.map(([key, value]) => [key, String(value)]),
78+
);
79+
}
80+
81+
if (err) {
82+
metricsReporter.reportError(err, data);
83+
}
84+
}

components/dashboard/src/service/public-api.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,27 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { createConnectTransport, createPromiseClient } from "@bufbuild/connect-web";
7+
import { createPromiseClient } from "@bufbuild/connect";
8+
import { createConnectTransport } from "@bufbuild/connect-web";
89
import { Project as ProtocolProject, Team as ProtocolTeam } from "@gitpod/gitpod-protocol/lib/teams-projects-protocol";
10+
import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connectweb";
911
import { TeamsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_connectweb";
1012
import { TokensService } from "@gitpod/public-api/lib/gitpod/experimental/v1/tokens_connectweb";
1113
import { ProjectsService } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_connectweb";
1214
import { WorkspacesService } from "@gitpod/public-api/lib/gitpod/experimental/v1/workspaces_connectweb";
1315
import { OIDCService } from "@gitpod/public-api/lib/gitpod/experimental/v1/oidc_connectweb";
16+
import { getMetricsInterceptor } from "@gitpod/public-api/lib/metrics";
1417
import { Team } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb";
1518
import { TeamMemberInfo, TeamMemberRole } from "@gitpod/gitpod-protocol";
1619
import { TeamMember, TeamRole } from "@gitpod/public-api/lib/gitpod/experimental/v1/teams_pb";
1720
import { Project } from "@gitpod/public-api/lib/gitpod/experimental/v1/projects_pb";
1821

1922
const transport = createConnectTransport({
2023
baseUrl: `${window.location.protocol}//${window.location.host}/public-api`,
24+
interceptors: [getMetricsInterceptor()],
2125
});
2226

27+
export const helloService = createPromiseClient(HelloService, transport);
2328
export const teamsService = createPromiseClient(TeamsService, transport);
2429
export const personalAccessTokensService = createPromiseClient(TokensService, transport);
2530
export const projectsService = createPromiseClient(ProjectsService, transport);

components/dashboard/src/service/service.tsx

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url"
1919
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
2020
import { IDEFrontendDashboardService } from "@gitpod/gitpod-protocol/lib/frontend-dashboard-service";
2121
import { RemoteTrackMessage } from "@gitpod/gitpod-protocol/lib/analytics";
22+
import { helloService } from "./public-api";
23+
import { getExperimentsClient } from "../experiments/client";
2224

2325
export const gitpodHostUrl = new GitpodHostUrl(window.location.toString());
2426

@@ -56,10 +58,81 @@ export function getGitpodService(): GitpodService {
5658
const service = _gp.gitpodService || (_gp.gitpodService = require("./service-mock").gitpodServiceMock);
5759
return service;
5860
}
59-
const service = _gp.gitpodService || (_gp.gitpodService = createGitpodService());
61+
let service = _gp.gitpodService;
62+
if (!service) {
63+
service = _gp.gitpodService = createGitpodService();
64+
testPublicAPI(service);
65+
}
6066
return service;
6167
}
6268

69+
/**
70+
* Emulates getWorkspace calls and listen to workspace statuses with Public API.
71+
* // TODO(ak): remove after reliability of Public API is confirmed
72+
*/
73+
function testPublicAPI(service: any): void {
74+
let user: any;
75+
service.server = new Proxy(service.server, {
76+
get(target, propKey) {
77+
return async function (...args: any[]) {
78+
if (propKey === "getLoggedInUser") {
79+
user = await target[propKey](...args);
80+
return user;
81+
}
82+
if (propKey === "getWorkspace") {
83+
try {
84+
return await target[propKey](...args);
85+
} finally {
86+
const grpcType = "unary";
87+
// emulates frequent unary calls to public API
88+
const isTest = await getExperimentsClient().getValueAsync(
89+
"public_api_dummy_reliability_test",
90+
false,
91+
{
92+
user,
93+
gitpodHost: window.location.host,
94+
},
95+
);
96+
if (isTest) {
97+
helloService.sayHello({}).catch((e) => {
98+
console.error(e, {
99+
userId: user?.id,
100+
workspaceId: args[0],
101+
grpcType,
102+
});
103+
});
104+
}
105+
}
106+
}
107+
return target[propKey](...args);
108+
};
109+
},
110+
});
111+
(async () => {
112+
const grpcType = "server-stream";
113+
// emulates server side streaming with public API
114+
while (true) {
115+
const isTest = await getExperimentsClient().getValueAsync("public_api_dummy_reliability_test", false, {
116+
user,
117+
gitpodHost: window.location.host,
118+
});
119+
if (isTest) {
120+
try {
121+
let previousCount = 0;
122+
for await (const reply of helloService.lotsOfReplies({ previousCount })) {
123+
previousCount = reply.count;
124+
}
125+
} catch (e) {
126+
console.error(e, {
127+
userId: user?.id,
128+
grpcType,
129+
});
130+
}
131+
}
132+
await new Promise((resolve) => setTimeout(resolve, 3000));
133+
}
134+
})();
135+
}
63136
let ideFrontendService: IDEFrontendService | undefined;
64137
export function getIDEFrontendService(workspaceID: string, sessionId: string, service: GitpodService) {
65138
if (!ideFrontendService) {

components/dashboard/src/teams/NewTeam.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { ConnectError } from "@bufbuild/connect-web";
7+
import { ConnectError } from "@bufbuild/connect";
88
import { FormEvent, useState } from "react";
99
import { useHistory } from "react-router-dom";
1010
import { Heading1, Heading3, Subheading } from "../components/typography/headings";

components/proxy/conf/Caddyfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,17 @@ https://{$GITPOD_DOMAIN} {
195195
import ssl_configuration
196196
import security_headers
197197

198+
@proxy_server_public_api path /public-api/gitpod.experimental.v1.HelloService*
199+
handle @proxy_server_public_api {
200+
uri strip_prefix /public-api
201+
# TODO(ak) verify that it only enabled for json content-type, not grpc
202+
import compression
203+
204+
reverse_proxy server.{$KUBE_NAMESPACE}.{$KUBE_DOMAIN}:3001 {
205+
import upstream_connection
206+
}
207+
}
208+
198209
@proxy_public_api path /public-api*
199210
handle @proxy_public_api {
200211
uri strip_prefix /public-api
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
syntax = "proto3";
2+
3+
package gitpod.experimental.v1;
4+
5+
option go_package = "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1";
6+
7+
// HelloService is a dummy service that says hello. It is used for reliability
8+
// testing.
9+
service HelloService {
10+
// Unary RPCs where the client sends a single request to the server and gets a
11+
// single response back, just like a normal function call.
12+
rpc SayHello(SayHelloRequest) returns (SayHelloResponse);
13+
// Server streaming RPCs where the client sends a request to the server and
14+
// gets a stream to read a sequence of messages back.
15+
rpc LotsOfReplies(LotsOfRepliesRequest)
16+
returns (stream LotsOfRepliesResponse);
17+
}
18+
19+
message SayHelloRequest {}
20+
message SayHelloResponse { string reply = 1; }
21+
22+
message LotsOfRepliesRequest {
23+
int32 previous_count = 1;
24+
}
25+
message LotsOfRepliesResponse {
26+
string reply = 1;
27+
int32 count = 2;
28+
}

0 commit comments

Comments
 (0)