Skip to content

Commit ff65c13

Browse files
Merge master into release
2 parents 8fb372a + 43a8d99 commit ff65c13

File tree

8 files changed

+184
-32
lines changed

8 files changed

+184
-32
lines changed

.changeset/cold-brooms-run.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/analytics': patch
3+
---
4+
5+
Analytics - fixed an issue where setConsent was clobbering the consentSettings before passing them to the gtag implementation.

.changeset/nine-rings-jog.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@firebase/firestore': patch
3+
'firebase': patch
4+
---
5+
6+
Fix multi-tab persistence raising empty snapshot issue

.github/workflows/release-prod.yml

+32
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,35 @@ jobs:
124124
curl -X POST -H "Content-Type:application/json" \
125125
-d "{\"version\":\"$BASE_VERSION\",\"date\":\"$DATE\"}" \
126126
$RELEASE_TRACKER_URL/logProduction
127+
- name: Create Github release
128+
env:
129+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
130+
run: |
131+
# Get the newest release tag for the firebase package (e.g. [email protected])
132+
NEWEST_TAG=$(git describe --tags --match "firebase@[0-9]*.[0-9]*.[0-9]*" --abbrev=0)
133+
134+
# Get the release notes from the description of the most recent merged PR into the "release" branch
135+
# See: https://github.com/firebase/firebase-js-sdk/pull/8236 for an example description
136+
JSON_RELEASE_NOTES=$(gh pr list \
137+
--repo "$GITHUB_REPOSITORY" \
138+
--state "merged" \
139+
--base "release" \
140+
--limit 1 \
141+
--json "body" \
142+
| jq '.[].body | split("\n# Releases\n")[-1]' # Remove the generated changesets header
143+
)
144+
145+
# Prepend the new release header
146+
# We have to be careful to insert the new release header after a " character, since we're
147+
# modifying the JSON string
148+
JSON_RELEASE_NOTES="\"For more detailed release notes, see [Firebase JavaScript SDK Release Notes](https://firebase.google.com/support/release-notes/js).\n\n# What's Changed\n\n${JSON_RELEASE_NOTES:1}"
149+
150+
# Format the JSON string into a readable markdown string
151+
RELEASE_NOTES=$(echo -E $JSON_RELEASE_NOTES | jq -r .)
152+
153+
# Create the GitHub release
154+
gh release create "$NEWEST_TAG" \
155+
--repo "$GITHUB_REPOSITORY" \
156+
--title "$NEWEST_TAG" \
157+
--notes "$RELEASE_NOTES" \
158+
--verify-tag

README.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ To get started using Firebase, see
1818
[![Release Notes](https://img.shields.io/npm/v/firebase.svg?style=flat-square&label=Release%20Notes%20for&labelColor=039be5&color=666)](https://firebase.google.com/support/release-notes/js)
1919

2020
## Upgrade to Version 9
21+
2122
Version 9 has a redesigned API that supports tree-shaking. Read the [Upgrade Guide](https://firebase.google.com/docs/web/modular-upgrade) to learn more.
23+
2224
## Supported Environments
25+
2326
Please see [Environment Support](https://firebase.google.com/support/guides/environments_js-sdk).
2427

2528
## SDK Dev Workflow
@@ -30,7 +33,7 @@ Please see [Environment Support](https://firebase.google.com/support/guides/envi
3033

3134
Before you can start working on the Firebase JS SDK, you need to have Node.js
3235
installed on your machine. As of April 19th, 2024 the team has been testing with Node.js version
33-
`20.12.2`, but the required verison of Node.js may change as we update our dependencies.
36+
`20.12.2`, but the required version of Node.js may change as we update our dependencies.
3437

3538
To download Node.js visit https://nodejs.org/en/download/.
3639

@@ -44,7 +47,7 @@ In addition to Node.js we use `yarn` to facilitate multi package development.
4447
To install `yarn` follow the instructions listed on their website:
4548
https://yarnpkg.com/en/docs/install
4649

47-
This repo currently supports building with yarn `1.x`. For instance, after installating yarn, run
50+
This repo currently supports building with yarn `1.x`. For instance, after installing yarn, run
4851
```bash
4952
$ yarn set version 1.22.11
5053
```
@@ -204,7 +207,7 @@ In order to manually test your SDK changes locally, you must use [yarn link](htt
204207
```shell
205208
$ cd packages/firebase
206209
$ yarn link # initialize the linking to the other folder
207-
$ cd ../packages/<my-product> # Example: $ cd packages/database
210+
$ cd ../<my-product> # Example: $ cd ../firestore
208211
$ yarn link # link your product to make it available elsewhere
209212
$ cd <my-test-app-dir> # cd into your personal project directory
210213
$ yarn link firebase @firebase/<my-product> # tell yarn to use the locally built firebase SDK instead

packages/analytics/src/helpers.test.ts

+87-25
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,11 @@ describe('Gtag wrapping functions', () => {
175175
'gtag'
176176
);
177177
window['dataLayer'] = [];
178-
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', {
178+
const eventObject = {
179179
'transaction_id': 'abcd123',
180180
'send_to': 'some_group'
181-
});
181+
};
182+
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', eventObject);
182183
expect((window['dataLayer'] as DataLayer).length).to.equal(0);
183184

184185
initPromise1.resolve(fakeMeasurementId); // Resolves first initialization promise.
@@ -187,8 +188,12 @@ describe('Gtag wrapping functions', () => {
187188
initPromise2.resolve('other-measurement-id'); // Resolves second initialization promise.
188189
await Promise.all([initPromise1, initPromise2]); // Wait for resolution of Promise.all()
189190
await promiseAllSettled(fakeDynamicConfigPromises);
190-
191-
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
191+
const dataLayer = window['dataLayer'] as DataLayer;
192+
expect(dataLayer.length).to.equal(1);
193+
const data = dataLayer[0];
194+
expect(data[0]).to.equal('event');
195+
expect(data[1]).to.equal('purchase');
196+
expect(data[2]).to.equal(eventObject);
192197
});
193198

194199
it(
@@ -208,10 +213,11 @@ describe('Gtag wrapping functions', () => {
208213
'gtag'
209214
);
210215
window['dataLayer'] = [];
211-
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', {
216+
const eventObject = {
212217
'transaction_id': 'abcd123',
213218
'send_to': [fakeMeasurementId, 'some_group']
214-
});
219+
};
220+
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', eventObject);
215221
expect((window['dataLayer'] as DataLayer).length).to.equal(0);
216222

217223
initPromise1.resolve(); // Resolves first initialization promise.
@@ -221,7 +227,12 @@ describe('Gtag wrapping functions', () => {
221227
await Promise.all([initPromise1, initPromise2]); // Wait for resolution of Promise.all()
222228
await promiseAllSettled(fakeDynamicConfigPromises);
223229

224-
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
230+
const dataLayer = window['dataLayer'] as DataLayer;
231+
expect(dataLayer.length).to.equal(1);
232+
const data = dataLayer[0];
233+
expect(data[0]).to.equal('event');
234+
expect(data[1]).to.equal('purchase');
235+
expect(data[2]).to.equal(eventObject);
225236
}
226237
);
227238

@@ -242,9 +253,10 @@ describe('Gtag wrapping functions', () => {
242253
'gtag'
243254
);
244255
window['dataLayer'] = [];
245-
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', {
256+
const eventObject = {
246257
'transaction_id': 'abcd123'
247-
});
258+
};
259+
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', eventObject);
248260
expect((window['dataLayer'] as DataLayer).length).to.equal(0);
249261

250262
initPromise1.resolve(); // Resolves first initialization promise.
@@ -253,7 +265,12 @@ describe('Gtag wrapping functions', () => {
253265
initPromise2.resolve(); // Resolves second initialization promise.
254266
await Promise.all([initPromise1, initPromise2]); // Wait for resolution of Promise.all()
255267

256-
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
268+
const dataLayer = window['dataLayer'] as DataLayer;
269+
expect(dataLayer.length).to.equal(1);
270+
const data = dataLayer[0];
271+
expect(data[0]).to.equal('event');
272+
expect(data[1]).to.equal('purchase');
273+
expect(data[2]).to.equal(eventObject);
257274
}
258275
);
259276

@@ -274,17 +291,23 @@ describe('Gtag wrapping functions', () => {
274291
'gtag'
275292
);
276293
window['dataLayer'] = [];
277-
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', {
294+
const eventObject = {
278295
'transaction_id': 'abcd123',
279296
'send_to': fakeMeasurementId
280-
});
297+
};
298+
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', eventObject);
281299
expect((window['dataLayer'] as DataLayer).length).to.equal(0);
282300

283301
initPromise1.resolve(); // Resolves first initialization promise.
284302
await promiseAllSettled(fakeDynamicConfigPromises);
285303
await Promise.all([initPromise1]); // Wait for resolution of Promise.all()
286304

287-
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
305+
const dataLayer = window['dataLayer'] as DataLayer;
306+
expect(dataLayer.length).to.equal(1);
307+
const data = dataLayer[0];
308+
expect(data[0]).to.equal('event');
309+
expect(data[1]).to.equal('purchase');
310+
expect(data[2]).to.equal(eventObject);
288311
}
289312
);
290313

@@ -307,8 +330,13 @@ describe('Gtag wrapping functions', () => {
307330
'gtag'
308331
);
309332
window['dataLayer'] = [];
310-
(window['gtag'] as Gtag)(GtagCommand.SET, { 'language': 'en' });
311-
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
333+
const eventObject = { 'language': 'en' };
334+
(window['gtag'] as Gtag)(GtagCommand.SET, eventObject);
335+
const dataLayer = window['dataLayer'] as DataLayer;
336+
expect(dataLayer.length).to.equal(1);
337+
const data = dataLayer[0];
338+
expect(data[0]).to.equal('set');
339+
expect(data[1]).to.equal(eventObject);
312340
});
313341

314342
it('new window.gtag function does not wait when sending "consent" calls', async () => {
@@ -329,7 +357,12 @@ describe('Gtag wrapping functions', () => {
329357
'update',
330358
consentParameters
331359
);
332-
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
360+
const dataLayer = window['dataLayer'] as DataLayer;
361+
expect(dataLayer.length).to.equal(1);
362+
const data = dataLayer[0];
363+
expect(data[0]).to.equal('consent');
364+
expect(data[1]).to.equal('update');
365+
expect(data[2]).to.equal(consentParameters);
333366
});
334367

335368
it('new window.gtag function does not wait when sending "get" calls', async () => {
@@ -347,7 +380,13 @@ describe('Gtag wrapping functions', () => {
347380
'client_id',
348381
clientId => console.log(clientId)
349382
);
350-
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
383+
const dataLayer = window['dataLayer'] as DataLayer;
384+
expect(dataLayer.length).to.equal(1);
385+
const data = dataLayer[0];
386+
expect(data[0]).to.equal('get');
387+
expect(data[1]).to.equal(fakeMeasurementId);
388+
expect(data[2]).to.equal('client_id');
389+
expect(data[3]).to.not.be.undefined;
351390
});
352391

353392
it('new window.gtag function does not wait when sending an unknown command', async () => {
@@ -360,7 +399,11 @@ describe('Gtag wrapping functions', () => {
360399
);
361400
window['dataLayer'] = [];
362401
(window['gtag'] as Gtag)('new-command-from-gtag-team', fakeMeasurementId);
363-
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
402+
const dataLayer = window['dataLayer'] as DataLayer;
403+
expect(dataLayer.length).to.equal(1);
404+
const data = dataLayer[0];
405+
expect(data[0]).to.equal('new-command-from-gtag-team');
406+
expect(data[1]).to.equal(fakeMeasurementId);
364407
});
365408

366409
it('new window.gtag function waits for initialization promise when sending "config" calls', async () => {
@@ -373,29 +416,48 @@ describe('Gtag wrapping functions', () => {
373416
'gtag'
374417
);
375418
window['dataLayer'] = [];
376-
(window['gtag'] as Gtag)(GtagCommand.CONFIG, fakeMeasurementId, {
419+
const eventObject = {
377420
'language': 'en'
378-
});
421+
};
422+
(window['gtag'] as Gtag)(
423+
GtagCommand.CONFIG,
424+
fakeMeasurementId,
425+
eventObject
426+
);
379427
expect((window['dataLayer'] as DataLayer).length).to.equal(0);
380428

381429
initPromise1.resolve(fakeMeasurementId);
382430
await promiseAllSettled(fakeDynamicConfigPromises); // Resolves dynamic config fetches.
383431
expect((window['dataLayer'] as DataLayer).length).to.equal(0);
384432

385433
await Promise.all([initPromise1]); // Wait for resolution of Promise.all()
386-
387-
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
434+
const dataLayer = window['dataLayer'] as DataLayer;
435+
expect(dataLayer.length).to.equal(1);
436+
const data = dataLayer[0];
437+
expect(data[0]).to.equal('config');
438+
expect(data[1]).to.equal(fakeMeasurementId);
439+
expect(data[2]).to.equal(eventObject);
388440
});
389441

390442
it('new window.gtag function does not wait when sending "config" calls if there are no pending initialization promises', async () => {
391443
wrapOrCreateGtag({}, fakeDynamicConfigPromises, {}, 'dataLayer', 'gtag');
392444
window['dataLayer'] = [];
393-
(window['gtag'] as Gtag)(GtagCommand.CONFIG, fakeMeasurementId, {
445+
const eventObject = {
394446
'transaction_id': 'abcd123'
395-
});
447+
};
448+
(window['gtag'] as Gtag)(
449+
GtagCommand.CONFIG,
450+
fakeMeasurementId,
451+
eventObject
452+
);
396453
await promiseAllSettled(fakeDynamicConfigPromises);
397454
await Promise.resolve(); // Config call is always chained onto initialization promise list, even if empty.
398-
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
455+
const dataLayer = window['dataLayer'] as DataLayer;
456+
expect(dataLayer.length).to.equal(1);
457+
const data = dataLayer[0];
458+
expect(data[0]).to.equal('config');
459+
expect(data[1]).to.equal(fakeMeasurementId);
460+
expect(data[2]).to.equal(eventObject);
399461
});
400462
});
401463

packages/analytics/src/helpers.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,13 @@ function wrapGtag(
304304
gtagParams as GtagConfigOrEventParams
305305
);
306306
} else if (command === GtagCommand.CONSENT) {
307-
const [gtagParams] = args;
308-
gtagCore(GtagCommand.CONSENT, 'update', gtagParams as ConsentSettings);
307+
const [consentAction, gtagParams] = args;
308+
// consentAction can be one of 'default' or 'update'.
309+
gtagCore(
310+
GtagCommand.CONSENT,
311+
consentAction,
312+
gtagParams as ConsentSettings
313+
);
309314
} else if (command === GtagCommand.GET) {
310315
const [measurementId, fieldName, callback] = args;
311316
gtagCore(

packages/firestore/src/core/sync_engine_impl.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1095,9 +1095,10 @@ export async function syncEngineEmitNewSnapsAndNotifyLocalStore(
10951095
// secondary clients to update query state.
10961096
if (viewSnapshot || remoteEvent) {
10971097
if (syncEngineImpl.isPrimaryClient) {
1098+
const isCurrent = viewSnapshot && !viewSnapshot.fromCache;
10981099
syncEngineImpl.sharedClientState.updateQueryState(
10991100
queryView.targetId,
1100-
viewSnapshot?.fromCache ? 'not-current' : 'current'
1101+
isCurrent ? 'current' : 'not-current'
11011102
);
11021103
}
11031104
}

packages/firestore/test/unit/specs/query_spec.test.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { Document } from '../../../src/model/document';
2424
import { doc, filter, query } from '../../util/helpers';
2525

2626
import { describeSpec, specTest } from './describe_spec';
27-
import { spec, SpecBuilder } from './spec_builder';
27+
import { client, spec, SpecBuilder } from './spec_builder';
2828

2929
// Helper to seed the cache with the specified docs by listening to each one.
3030
function specWithCachedDocs(...docs: Document[]): SpecBuilder {
@@ -136,4 +136,42 @@ describeSpec('Queries:', [], () => {
136136
);
137137
}
138138
);
139+
140+
specTest(
141+
'Queries in different tabs will not interfere',
142+
['multi-client'],
143+
() => {
144+
const query1 = query('collection', filter('key', '==', 'a'));
145+
const query2 = query('collection', filter('key', '==', 'b'));
146+
const docA = doc('collection/a', 1000, { key: 'a' });
147+
const docB = doc('collection/b', 1000, { key: 'b' });
148+
149+
return (
150+
client(0)
151+
.becomeVisible()
152+
// Listen to the first query in the primary client
153+
.expectPrimaryState(true)
154+
.userListens(query1)
155+
.watchAcks(query1)
156+
.watchSends({ affects: [query1] }, docA)
157+
158+
// Listen to different query in the secondary client
159+
.client(1)
160+
.userListens(query2)
161+
162+
.client(0)
163+
.expectListen(query2)
164+
.watchCurrents(query1, 'resume-token-1000')
165+
// Receive global snapshot before the second query is acknowledged
166+
.watchSnapshots(1000)
167+
.expectEvents(query1, { added: [docA] })
168+
// This should not trigger empty snapshot for second query(bugged behavior)
169+
.client(1)
170+
.client(0)
171+
.watchAcksFull(query2, 2000, docB)
172+
.client(1)
173+
.expectEvents(query2, { added: [docB] })
174+
);
175+
}
176+
);
139177
});

0 commit comments

Comments
 (0)