Skip to content

Commit 329dc8f

Browse files
ergunshDevtools-frontend LUCI CQ
authored andcommitted
[GdpIntegration] Add GdpClient for getting & creating a GDP profile
Bug: 436202677, 441467083 Fixed: 441436892 Change-Id: I0e143e14f2a8c4db1f80b053ba8056499ec019bc Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6894997 Auto-Submit: Ergün Erdoğmuş <[email protected]> Reviewed-by: Alex Rudenko <[email protected]> Commit-Queue: Ergün Erdoğmuş <[email protected]>
1 parent abfc24e commit 329dc8f

File tree

7 files changed

+217
-1
lines changed

7 files changed

+217
-1
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,7 @@ grd_files_unbundled_sources = [
851851
"front_end/core/common/Worker.js",
852852
"front_end/core/dom_extension/DOMExtension.js",
853853
"front_end/core/host/AidaClient.js",
854+
"front_end/core/host/GdpClient.js",
854855
"front_end/core/host/InspectorFrontendHost.js",
855856
"front_end/core/host/InspectorFrontendHostAPI.js",
856857
"front_end/core/host/Platform.js",

front_end/core/host/BUILD.gn

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import("../../../scripts/build/ninja/devtools_module.gni")
88
devtools_module("host") {
99
sources = [
1010
"AidaClient.ts",
11+
"GdpClient.ts",
1112
"InspectorFrontendHost.ts",
1213
"InspectorFrontendHostAPI.ts",
1314
"Platform.ts",
@@ -34,7 +35,10 @@ devtools_entrypoint("bundle") {
3435
ts_library("unittests") {
3536
testonly = true
3637

37-
sources = [ "AidaClient.test.ts" ]
38+
sources = [
39+
"AidaClient.test.ts",
40+
"GdpClient.test.ts",
41+
]
3842

3943
deps = [
4044
":bundle",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2025 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as Host from './host.js';
6+
7+
describe('GdpClient', () => {
8+
let dispatchHttpRequestStub:
9+
sinon.SinonStub<Parameters<typeof Host.InspectorFrontendHost.InspectorFrontendHostInstance.dispatchHttpRequest>>;
10+
beforeEach(() => {
11+
dispatchHttpRequestStub =
12+
sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'dispatchHttpRequest')
13+
.callsFake((request, cb) => {
14+
cb({
15+
response: JSON.stringify({name: 'profiles/id'}),
16+
statusCode: 200,
17+
});
18+
});
19+
Host.GdpClient.GdpClient.instance({forceNew: true});
20+
});
21+
22+
it('should cache requests to getProfile', async () => {
23+
await Host.GdpClient.GdpClient.instance().getProfile();
24+
await Host.GdpClient.GdpClient.instance().getProfile();
25+
26+
sinon.assert.calledOnce(dispatchHttpRequestStub);
27+
});
28+
29+
it('should cache requests to checkEligibility', async () => {
30+
await Host.GdpClient.GdpClient.instance().checkEligibility();
31+
await Host.GdpClient.GdpClient.instance().checkEligibility();
32+
33+
sinon.assert.calledOnce(dispatchHttpRequestStub);
34+
});
35+
});

front_end/core/host/GdpClient.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright 2025 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import {InspectorFrontendHostInstance} from './InspectorFrontendHost.js';
6+
import type {DispatchHttpRequestRequest, DispatchHttpRequestResult} from './InspectorFrontendHostAPI.js';
7+
8+
enum SubscriptionStatus {
9+
ENABLED = 'SUBSCRIPTION_STATE_ENABLED',
10+
PENDING = 'SUBSCRIPTION_STATE_PENDING',
11+
CANCELED = 'SUBSCRIPTION_STATE_CANCELED',
12+
REFUNDED = 'SUBSCRIPTION_STATE_REFUNDED',
13+
AWAITING_FIX = 'SUBSCRIPTION_STATE_AWAITING_FIX',
14+
ON_HOLD = 'SUBSCRIPTION_STATE_ACCOUNT_ON_HOLD',
15+
}
16+
17+
enum SubscriptionTier {
18+
PREMIUM_ANNUAL = 'SUBSCRIPTION_TIER_PREMIUM_ANNUAL',
19+
PREMIUM_MONTHLY = 'SUBSCRIPTION_TIER_PREMIUM_MONTHLY',
20+
PRO_ANNUAL = 'SUBSCRIPTION_TIER_PRO_ANNUAL',
21+
PRO_MONTHLY = 'SUBSCRIPTION_TIER_PRO_MONTHLY',
22+
}
23+
24+
enum EligibilityStatus {
25+
ELIGIBLE = 'ELIGIBLE',
26+
NOT_ELIGIBLE = 'NOT_ELIGIBLE',
27+
}
28+
29+
enum EmailPreference {
30+
ENABLED = 'ENABLED',
31+
DISABLED = 'DISABLED',
32+
}
33+
34+
interface CheckElibigilityResponse {
35+
createProfile: EligibilityStatus;
36+
}
37+
38+
interface Profile {
39+
// Resource name of the profile.
40+
// Format: profiles/{obfuscated_profile_id}
41+
name: string;
42+
activeSubscription?: {
43+
subscriptionStatus: SubscriptionStatus,
44+
subscrionTier: SubscriptionTier,
45+
};
46+
}
47+
48+
async function dispatchHttpRequestPromise<R extends object>(request: DispatchHttpRequestRequest): Promise<R|null> {
49+
const response = await new Promise<DispatchHttpRequestResult>(resolve => {
50+
InspectorFrontendHostInstance.dispatchHttpRequest(request, resolve);
51+
});
52+
53+
debugLog({request, response});
54+
if ('response' in response && response.statusCode === 200) {
55+
return JSON.parse(response.response) as R;
56+
}
57+
58+
return null;
59+
}
60+
61+
const SERVICE_NAME = 'gdpService';
62+
let gdpClientInstance: GdpClient|null = null;
63+
export class GdpClient {
64+
#cachedProfilePromise?: Promise<Profile|null>;
65+
#cachedEligibilityPromise?: Promise<CheckElibigilityResponse|null>;
66+
67+
private constructor() {
68+
}
69+
70+
static instance({forceNew}: {
71+
forceNew: boolean,
72+
} = {forceNew: false}): GdpClient {
73+
if (!gdpClientInstance || forceNew) {
74+
gdpClientInstance = new GdpClient();
75+
}
76+
return gdpClientInstance;
77+
}
78+
79+
async getProfile(): Promise<Profile|null> {
80+
if (this.#cachedProfilePromise) {
81+
return await this.#cachedProfilePromise;
82+
}
83+
84+
this.#cachedProfilePromise = dispatchHttpRequestPromise({
85+
service: SERVICE_NAME,
86+
path: '/v1beta1/profile:get',
87+
method: 'GET',
88+
});
89+
return await this.#cachedProfilePromise;
90+
}
91+
92+
async checkEligibility(): Promise<CheckElibigilityResponse|null> {
93+
if (this.#cachedEligibilityPromise) {
94+
return await this.#cachedEligibilityPromise;
95+
}
96+
97+
this.#cachedEligibilityPromise =
98+
dispatchHttpRequestPromise({service: SERVICE_NAME, path: '/v1beta1/eligibility:check', method: 'GET'});
99+
100+
return await this.#cachedEligibilityPromise;
101+
}
102+
103+
createProfile({user, emailPreference}: {user: string, emailPreference: EmailPreference}): Promise<Profile|null> {
104+
return dispatchHttpRequestPromise({
105+
service: SERVICE_NAME,
106+
path: '/v1beta1/profiles',
107+
method: 'POST',
108+
body: JSON.stringify({
109+
user,
110+
newsletter_email: emailPreference,
111+
})
112+
});
113+
}
114+
}
115+
116+
function isDebugMode(): boolean {
117+
return Boolean(localStorage.getItem('debugGdpIntegrationEnabled'));
118+
}
119+
120+
function debugLog(...log: unknown[]): void {
121+
if (!isDebugMode()) {
122+
return;
123+
}
124+
125+
// eslint-disable-next-line no-console
126+
console.log('debugLog', ...log);
127+
}
128+
129+
function setDebugGdpIntegrationEnabled(enabled: boolean): void {
130+
if (enabled) {
131+
localStorage.setItem('debugGdpIntegrationEnabled', 'true');
132+
} else {
133+
localStorage.removeItem('debugGdpIntegrationEnabled');
134+
}
135+
}
136+
137+
// @ts-expect-error
138+
globalThis.setDebugGdpIntegrationEnabled = setDebugGdpIntegrationEnabled;

front_end/core/host/InspectorFrontendHost.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import {
4040
type ChangeEvent,
4141
type ClickEvent,
4242
type ContextMenuDescriptor,
43+
type DispatchHttpRequestRequest,
44+
type DispatchHttpRequestResult,
4345
type DoAidaConversationResult,
4446
type DragEvent,
4547
type EnumeratedHistogram,
@@ -548,6 +550,11 @@ export class InspectorFrontendHostStub implements InspectorFrontendHostAPI {
548550
});
549551
}
550552

553+
dispatchHttpRequest(_request: DispatchHttpRequestRequest, callback: (result: DispatchHttpRequestResult) => void):
554+
void {
555+
callback({error: 'Not implemented'});
556+
}
557+
551558
recordImpression(_event: ImpressionEvent): void {
552559
}
553560
recordResize(_event: ResizeEvent): void {

front_end/core/host/InspectorFrontendHostAPI.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,34 @@ export interface EventTypes {
253253
[Events.ShowPanel]: string;
254254
}
255255

256+
export type DispatchHttpRequestRequest = {
257+
service: string,
258+
path: string,
259+
method: 'GET',
260+
body?: never,
261+
}|{
262+
service: string,
263+
path: string,
264+
method: 'POST',
265+
// A JSON string containing the request body.
266+
body?: string,
267+
};
268+
269+
interface DispatchHttpRequestSuccessResult {
270+
response: string;
271+
statusCode: number;
272+
}
273+
274+
interface DispatchHttpRequestErrorResult {
275+
error: string;
276+
detail?: string;
277+
netError?: number;
278+
netErrorName?: string;
279+
statusCode?: number;
280+
}
281+
282+
export type DispatchHttpRequestResult = DispatchHttpRequestSuccessResult|DispatchHttpRequestErrorResult;
283+
256284
export interface InspectorFrontendHostAPI {
257285
events: Common.EventTarget.EventTarget<EventTypes>;
258286

@@ -402,6 +430,7 @@ export interface InspectorFrontendHostAPI {
402430
doAidaConversation: (request: string, streamId: number, cb: (result: DoAidaConversationResult) => void) => void;
403431
registerAidaClientEvent: (request: string, cb: (result: AidaClientResult) => void) => void;
404432
aidaCodeComplete: (request: string, cb: (result: AidaCodeCompleteResult) => void) => void;
433+
dispatchHttpRequest: (request: DispatchHttpRequestRequest, cb: (result: DispatchHttpRequestResult) => void) => void;
405434

406435
recordImpression(event: ImpressionEvent): void;
407436
recordClick(event: ClickEvent): void;

front_end/core/host/host.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import * as AidaClient from './AidaClient.js';
6+
import * as GdpClient from './GdpClient.js';
67
import * as InspectorFrontendHost from './InspectorFrontendHost.js';
78
import * as InspectorFrontendHostAPI from './InspectorFrontendHostAPI.js';
89
import * as Platform from './Platform.js';
@@ -11,6 +12,7 @@ import * as UserMetrics from './UserMetrics.js';
1112

1213
export {
1314
AidaClient,
15+
GdpClient,
1416
InspectorFrontendHost,
1517
InspectorFrontendHostAPI,
1618
Platform,

0 commit comments

Comments
 (0)