Skip to content

Commit 313faf6

Browse files
Added more granular tracking for web frameworks
1 parent 721e5a7 commit 313faf6

File tree

7 files changed

+195
-20
lines changed

7 files changed

+195
-20
lines changed

.changeset/little-news-sniff.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@firebase/data-connect': minor
3+
'firebase': minor
4+
---
5+
6+
Add custom request headers based on the type of SDK (JS/TS, React, Angular, etc) that's invoking Data Connect requests. This will help us understand how users interact with Data Connect when using the Web SDK.

common/api-review/data-connect.api.md

+13
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ import { FirebaseError } from '@firebase/util';
1111
import { LogLevelString } from '@firebase/logger';
1212
import { Provider } from '@firebase/component';
1313

14+
// @public
15+
export type CallerSdkType = 'Base' | 'Generated' | 'TanstackReactCore' | 'GeneratedReact' | 'TanstackAngularCore' | 'GeneratedAngular';
16+
17+
// @public (undocumented)
18+
export const CallerSdkTypeEnum: {
19+
readonly Base: "Base";
20+
readonly Generated: "Generated";
21+
readonly TanstackReactCore: "TanstackReactCore";
22+
readonly GeneratedReact: "GeneratedReact";
23+
readonly TanstackAngularCore: "TanstackAngularCore";
24+
readonly GeneratedAngular: "GeneratedAngular";
25+
};
26+
1427
// @public
1528
export function connectDataConnectEmulator(dc: DataConnect, host: string, port?: number, sslEnabled?: boolean): void;
1629

packages/data-connect/src/api/DataConnect.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ import {
3333
} from '../core/FirebaseAuthProvider';
3434
import { QueryManager } from '../core/QueryManager';
3535
import { logDebug, logError } from '../logger';
36-
import { DataConnectTransport, TransportClass } from '../network';
36+
import {
37+
CallerSdkType,
38+
CallerSdkTypeEnum,
39+
DataConnectTransport,
40+
TransportClass
41+
} from '../network';
3742
import { RESTTransport } from '../network/transport/rest';
3843

3944
import { MutationManager } from './Mutation';
@@ -92,6 +97,7 @@ export class DataConnect {
9297
private _transportOptions?: TransportOptions;
9398
private _authTokenProvider?: AuthTokenProvider;
9499
_isUsingGeneratedSdk: boolean = false;
100+
_callerSdkType: CallerSdkType = CallerSdkTypeEnum.Base;
95101
private _appCheckTokenProvider?: AppCheckTokenProvider;
96102
// @internal
97103
constructor(
@@ -116,6 +122,12 @@ export class DataConnect {
116122
this._isUsingGeneratedSdk = true;
117123
}
118124
}
125+
_setCallerSdkType(callerSdkType: CallerSdkType): void {
126+
this._callerSdkType = callerSdkType;
127+
if (this._initialized) {
128+
this._transport._setCallerSdkType(callerSdkType);
129+
}
130+
}
119131
_delete(): Promise<void> {
120132
_removeServiceInstance(
121133
this.app,
@@ -164,7 +176,8 @@ export class DataConnect {
164176
this._authTokenProvider,
165177
this._appCheckTokenProvider,
166178
undefined,
167-
this._isUsingGeneratedSdk
179+
this._isUsingGeneratedSdk,
180+
this._callerSdkType
168181
);
169182
if (this._transportOptions) {
170183
this._transport.useEmulator(

packages/data-connect/src/network/fetch.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,23 @@ import { Code, DataConnectError } from '../core/error';
1919
import { SDK_VERSION } from '../core/version';
2020
import { logDebug, logError } from '../logger';
2121

22+
import { CallerSdkType, CallerSdkTypeEnum } from './transport';
23+
2224
let connectFetch: typeof fetch | null = globalThis.fetch;
2325
export function initializeFetch(fetchImpl: typeof fetch): void {
2426
connectFetch = fetchImpl;
2527
}
26-
function getGoogApiClientValue(_isUsingGen: boolean): string {
28+
function getGoogApiClientValue(
29+
_isUsingGen: boolean,
30+
_callerSdkType: CallerSdkType
31+
): string {
2732
let str = 'gl-js/ fire/' + SDK_VERSION;
28-
if (_isUsingGen) {
33+
if (
34+
_callerSdkType !== CallerSdkTypeEnum.Base &&
35+
_callerSdkType !== CallerSdkTypeEnum.Generated
36+
) {
37+
str += ' js/' + _callerSdkType.toLowerCase();
38+
} else if (_isUsingGen || _callerSdkType === CallerSdkTypeEnum.Generated) {
2939
str += ' js/gen';
3040
}
3141
return str;
@@ -42,14 +52,15 @@ export function dcFetch<T, U>(
4252
appId: string | null,
4353
accessToken: string | null,
4454
appCheckToken: string | null,
45-
_isUsingGen: boolean
55+
_isUsingGen: boolean,
56+
_callerSdkType: CallerSdkType
4657
): Promise<{ data: T; errors: Error[] }> {
4758
if (!connectFetch) {
4859
throw new DataConnectError(Code.OTHER, 'No Fetch Implementation detected!');
4960
}
5061
const headers: HeadersInit = {
5162
'Content-Type': 'application/json',
52-
'X-Goog-Api-Client': getGoogApiClientValue(_isUsingGen)
63+
'X-Goog-Api-Client': getGoogApiClientValue(_isUsingGen, _callerSdkType)
5364
};
5465
if (accessToken) {
5566
headers['X-Firebase-Auth-Token'] = accessToken;

packages/data-connect/src/network/transport/index.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,26 @@ import { DataConnectOptions, TransportOptions } from '../../api/DataConnect';
1919
import { AppCheckTokenProvider } from '../../core/AppCheckTokenProvider';
2020
import { AuthTokenProvider } from '../../core/FirebaseAuthProvider';
2121

22+
/**
23+
* enum representing different flavors of the SDK used by developers
24+
* use the CallerSdkType for type-checking, and the CallerSdkTypeEnum for value-checking/assigning
25+
*/
26+
export type CallerSdkType =
27+
| 'Base' // Core JS SDK
28+
| 'Generated' // Generated JS SDK
29+
| 'TanstackReactCore' // Tanstack non-generated React SDK
30+
| 'GeneratedReact' // Generated React SDK
31+
| 'TanstackAngularCore' // Tanstack non-generated Angular SDK
32+
| 'GeneratedAngular'; // Generated Angular SDK
33+
export const CallerSdkTypeEnum = {
34+
Base: 'Base', // Core JS SDK
35+
Generated: 'Generated', // Generated JS SDK
36+
TanstackReactCore: 'TanstackReactCore', // Tanstack non-generated React SDK
37+
GeneratedReact: 'GeneratedReact', // Tanstack non-generated Angular SDK
38+
TanstackAngularCore: 'TanstackAngularCore', // Tanstack non-generated Angular SDK
39+
GeneratedAngular: 'GeneratedAngular' // Generated Angular SDK
40+
} as const;
41+
2242
/**
2343
* @internal
2444
*/
@@ -33,6 +53,7 @@ export interface DataConnectTransport {
3353
): Promise<{ data: T; errors: Error[] }>;
3454
useEmulator(host: string, port?: number, sslEnabled?: boolean): void;
3555
onTokenChanged: (token: string | null) => void;
56+
_setCallerSdkType(callerSdkType: CallerSdkType): void;
3657
}
3758

3859
/**
@@ -45,5 +66,6 @@ export type TransportClass = new (
4566
authProvider?: AuthTokenProvider,
4667
appCheckProvider?: AppCheckTokenProvider,
4768
transportOptions?: TransportOptions,
48-
_isUsingGen?: boolean
69+
_isUsingGen?: boolean,
70+
_callerSdkType?: CallerSdkType
4971
) => DataConnectTransport;

packages/data-connect/src/network/transport/rest.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { logDebug } from '../../logger';
2323
import { addToken, urlBuilder } from '../../util/url';
2424
import { dcFetch } from '../fetch';
2525

26-
import { DataConnectTransport } from '.';
26+
import { CallerSdkType, CallerSdkTypeEnum, DataConnectTransport } from '.';
2727

2828
export class RESTTransport implements DataConnectTransport {
2929
private _host = '';
@@ -43,7 +43,8 @@ export class RESTTransport implements DataConnectTransport {
4343
private authProvider?: AuthTokenProvider | undefined,
4444
private appCheckProvider?: AppCheckTokenProvider | undefined,
4545
transportOptions?: TransportOptions | undefined,
46-
private _isUsingGen = false
46+
private _isUsingGen = false,
47+
private _callerSdkType: CallerSdkType = CallerSdkTypeEnum.Base
4748
) {
4849
if (transportOptions) {
4950
if (typeof transportOptions.port === 'number') {
@@ -180,7 +181,8 @@ export class RESTTransport implements DataConnectTransport {
180181
this.appId,
181182
this._accessToken,
182183
this._appCheckToken,
183-
this._isUsingGen
184+
this._isUsingGen,
185+
this._callerSdkType
184186
)
185187
);
186188
return withAuth;
@@ -205,9 +207,14 @@ export class RESTTransport implements DataConnectTransport {
205207
this.appId,
206208
this._accessToken,
207209
this._appCheckToken,
208-
this._isUsingGen
210+
this._isUsingGen,
211+
this._callerSdkType
209212
);
210213
});
211214
return taskResult;
212215
};
216+
217+
_setCallerSdkType(callerSdkType: CallerSdkType): void {
218+
this._callerSdkType = callerSdkType;
219+
}
213220
}

packages/data-connect/test/unit/fetch.test.ts

+112-9
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,30 @@ import chaiAsPromised from 'chai-as-promised';
2020
import * as sinon from 'sinon';
2121

2222
import { dcFetch, initializeFetch } from '../../src/network/fetch';
23+
import { CallerSdkType, CallerSdkTypeEnum } from '../../src/network/transport';
2324
use(chaiAsPromised);
24-
function mockFetch(json: object): void {
25+
function mockFetch(json: object, reject: boolean): sinon.SinonStub {
2526
const fakeFetchImpl = sinon.stub().returns(
2627
Promise.resolve({
2728
json: () => {
2829
return Promise.resolve(json);
2930
},
30-
status: 401
31+
status: reject ? 401 : 200
3132
} as Response)
3233
);
3334
initializeFetch(fakeFetchImpl);
35+
return fakeFetchImpl;
3436
}
3537
describe('fetch', () => {
3638
it('should throw an error with just the message when the server responds with an error with a message property in the body', async () => {
3739
const message = 'Failed to connect to Postgres instance';
38-
mockFetch({
39-
code: 401,
40-
message
41-
});
40+
mockFetch(
41+
{
42+
code: 401,
43+
message
44+
},
45+
true
46+
);
4247
await expect(
4348
dcFetch(
4449
'http://localhost',
@@ -51,7 +56,8 @@ describe('fetch', () => {
5156
null,
5257
null,
5358
null,
54-
false
59+
false,
60+
CallerSdkTypeEnum.Base
5561
)
5662
).to.eventually.be.rejectedWith(message);
5763
});
@@ -61,7 +67,7 @@ describe('fetch', () => {
6167
code: 401,
6268
message1: message
6369
};
64-
mockFetch(json);
70+
mockFetch(json, true);
6571
await expect(
6672
dcFetch(
6773
'http://localhost',
@@ -74,8 +80,105 @@ describe('fetch', () => {
7480
null,
7581
null,
7682
null,
77-
false
83+
false,
84+
CallerSdkTypeEnum.Base
7885
)
7986
).to.eventually.be.rejectedWith(JSON.stringify(json));
8087
});
88+
it('should assign different values to custom headers based on the _callerSdkType argument (_isUsingGen is false)', async () => {
89+
const json = {
90+
code: 200,
91+
message1: 'success'
92+
};
93+
const fakeFetchImpl = mockFetch(json, false);
94+
95+
for (const callerSdkType in CallerSdkTypeEnum) {
96+
// this check is done to follow the best practices outlined by the "guard-for-in" eslint rule
97+
if (
98+
Object.prototype.hasOwnProperty.call(CallerSdkTypeEnum, callerSdkType)
99+
) {
100+
await dcFetch(
101+
'http://localhost',
102+
{
103+
name: 'n',
104+
operationName: 'n',
105+
variables: {}
106+
},
107+
{} as AbortController,
108+
null,
109+
null,
110+
null,
111+
false, // _isUsingGen is false
112+
callerSdkType as CallerSdkType
113+
);
114+
115+
let expectedHeaderRegex: RegExp;
116+
if (callerSdkType === CallerSdkTypeEnum.Base) {
117+
// should not contain any "js/xxx" substring
118+
expectedHeaderRegex = RegExp(/^((?!js\/\w).)*$/);
119+
} else if (callerSdkType === CallerSdkTypeEnum.Generated) {
120+
expectedHeaderRegex = RegExp(/js\/gen/);
121+
} else {
122+
expectedHeaderRegex = RegExp(`js\/${callerSdkType.toLowerCase()}`);
123+
}
124+
expect(
125+
fakeFetchImpl.calledWithMatch(
126+
'http://localhost',
127+
sinon.match.hasNested(
128+
'headers.X-Goog-Api-Client',
129+
sinon.match(expectedHeaderRegex)
130+
)
131+
)
132+
).to.be.true;
133+
}
134+
}
135+
});
136+
it('should assign custom headers based on _callerSdkType before checking to-be-deprecated _isUsingGen', async () => {
137+
const json = {
138+
code: 200,
139+
message1: 'success'
140+
};
141+
const fakeFetchImpl = mockFetch(json, false);
142+
143+
for (const callerSdkType in CallerSdkTypeEnum) {
144+
// this check is done to follow the best practices outlined by the "guard-for-in" eslint rule
145+
if (
146+
Object.prototype.hasOwnProperty.call(CallerSdkTypeEnum, callerSdkType)
147+
) {
148+
await dcFetch(
149+
'http://localhost',
150+
{
151+
name: 'n',
152+
operationName: 'n',
153+
variables: {}
154+
},
155+
{} as AbortController,
156+
null,
157+
null,
158+
null,
159+
true, // _isUsingGen is true
160+
callerSdkType as CallerSdkType
161+
);
162+
163+
let expectedHeaderRegex: RegExp;
164+
if (
165+
callerSdkType === CallerSdkTypeEnum.Generated ||
166+
callerSdkType === CallerSdkTypeEnum.Base
167+
) {
168+
expectedHeaderRegex = RegExp(`js\/gen`);
169+
} else {
170+
expectedHeaderRegex = RegExp(`js\/${callerSdkType.toLowerCase()}`);
171+
}
172+
expect(
173+
fakeFetchImpl.calledWithMatch(
174+
'http://localhost',
175+
sinon.match.hasNested(
176+
'headers.X-Goog-Api-Client',
177+
sinon.match(expectedHeaderRegex)
178+
)
179+
)
180+
).to.be.true;
181+
}
182+
}
183+
});
81184
});

0 commit comments

Comments
 (0)