Skip to content

Commit

Permalink
Merge master into release
Browse files Browse the repository at this point in the history
  • Loading branch information
google-oss-bot authored May 20, 2024
2 parents 8fb372a + 43a8d99 commit ff65c13
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-brooms-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/analytics': patch
---

Analytics - fixed an issue where setConsent was clobbering the consentSettings before passing them to the gtag implementation.
6 changes: 6 additions & 0 deletions .changeset/nine-rings-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@firebase/firestore': patch
'firebase': patch
---

Fix multi-tab persistence raising empty snapshot issue
32 changes: 32 additions & 0 deletions .github/workflows/release-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,35 @@ jobs:
curl -X POST -H "Content-Type:application/json" \
-d "{\"version\":\"$BASE_VERSION\",\"date\":\"$DATE\"}" \
$RELEASE_TRACKER_URL/logProduction
- name: Create Github release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get the newest release tag for the firebase package (e.g. firebase@10.12.0)
NEWEST_TAG=$(git describe --tags --match "firebase@[0-9]*.[0-9]*.[0-9]*" --abbrev=0)
# Get the release notes from the description of the most recent merged PR into the "release" branch
# See: https://github.com/firebase/firebase-js-sdk/pull/8236 for an example description
JSON_RELEASE_NOTES=$(gh pr list \
--repo "$GITHUB_REPOSITORY" \
--state "merged" \
--base "release" \
--limit 1 \
--json "body" \
| jq '.[].body | split("\n# Releases\n")[-1]' # Remove the generated changesets header
)
# Prepend the new release header
# We have to be careful to insert the new release header after a " character, since we're
# modifying the JSON string
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}"
# Format the JSON string into a readable markdown string
RELEASE_NOTES=$(echo -E $JSON_RELEASE_NOTES | jq -r .)
# Create the GitHub release
gh release create "$NEWEST_TAG" \
--repo "$GITHUB_REPOSITORY" \
--title "$NEWEST_TAG" \
--notes "$RELEASE_NOTES" \
--verify-tag
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ To get started using Firebase, see
[![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)

## Upgrade to Version 9

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.

## Supported Environments

Please see [Environment Support](https://firebase.google.com/support/guides/environments_js-sdk).

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

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

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

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

This repo currently supports building with yarn `1.x`. For instance, after installating yarn, run
This repo currently supports building with yarn `1.x`. For instance, after installing yarn, run
```bash
$ yarn set version 1.22.11
```
Expand Down Expand Up @@ -204,7 +207,7 @@ In order to manually test your SDK changes locally, you must use [yarn link](htt
```shell
$ cd packages/firebase
$ yarn link # initialize the linking to the other folder
$ cd ../packages/<my-product> # Example: $ cd packages/database
$ cd ../<my-product> # Example: $ cd ../firestore
$ yarn link # link your product to make it available elsewhere
$ cd <my-test-app-dir> # cd into your personal project directory
$ yarn link firebase @firebase/<my-product> # tell yarn to use the locally built firebase SDK instead
Expand Down
112 changes: 87 additions & 25 deletions packages/analytics/src/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,11 @@ describe('Gtag wrapping functions', () => {
'gtag'
);
window['dataLayer'] = [];
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', {
const eventObject = {
'transaction_id': 'abcd123',
'send_to': 'some_group'
});
};
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', eventObject);
expect((window['dataLayer'] as DataLayer).length).to.equal(0);

initPromise1.resolve(fakeMeasurementId); // Resolves first initialization promise.
Expand All @@ -187,8 +188,12 @@ describe('Gtag wrapping functions', () => {
initPromise2.resolve('other-measurement-id'); // Resolves second initialization promise.
await Promise.all([initPromise1, initPromise2]); // Wait for resolution of Promise.all()
await promiseAllSettled(fakeDynamicConfigPromises);

expect((window['dataLayer'] as DataLayer).length).to.equal(1);
const dataLayer = window['dataLayer'] as DataLayer;
expect(dataLayer.length).to.equal(1);
const data = dataLayer[0];
expect(data[0]).to.equal('event');
expect(data[1]).to.equal('purchase');
expect(data[2]).to.equal(eventObject);
});

it(
Expand All @@ -208,10 +213,11 @@ describe('Gtag wrapping functions', () => {
'gtag'
);
window['dataLayer'] = [];
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', {
const eventObject = {
'transaction_id': 'abcd123',
'send_to': [fakeMeasurementId, 'some_group']
});
};
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', eventObject);
expect((window['dataLayer'] as DataLayer).length).to.equal(0);

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

expect((window['dataLayer'] as DataLayer).length).to.equal(1);
const dataLayer = window['dataLayer'] as DataLayer;
expect(dataLayer.length).to.equal(1);
const data = dataLayer[0];
expect(data[0]).to.equal('event');
expect(data[1]).to.equal('purchase');
expect(data[2]).to.equal(eventObject);
}
);

Expand All @@ -242,9 +253,10 @@ describe('Gtag wrapping functions', () => {
'gtag'
);
window['dataLayer'] = [];
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', {
const eventObject = {
'transaction_id': 'abcd123'
});
};
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', eventObject);
expect((window['dataLayer'] as DataLayer).length).to.equal(0);

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

expect((window['dataLayer'] as DataLayer).length).to.equal(1);
const dataLayer = window['dataLayer'] as DataLayer;
expect(dataLayer.length).to.equal(1);
const data = dataLayer[0];
expect(data[0]).to.equal('event');
expect(data[1]).to.equal('purchase');
expect(data[2]).to.equal(eventObject);
}
);

Expand All @@ -274,17 +291,23 @@ describe('Gtag wrapping functions', () => {
'gtag'
);
window['dataLayer'] = [];
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', {
const eventObject = {
'transaction_id': 'abcd123',
'send_to': fakeMeasurementId
});
};
(window['gtag'] as Gtag)(GtagCommand.EVENT, 'purchase', eventObject);
expect((window['dataLayer'] as DataLayer).length).to.equal(0);

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

expect((window['dataLayer'] as DataLayer).length).to.equal(1);
const dataLayer = window['dataLayer'] as DataLayer;
expect(dataLayer.length).to.equal(1);
const data = dataLayer[0];
expect(data[0]).to.equal('event');
expect(data[1]).to.equal('purchase');
expect(data[2]).to.equal(eventObject);
}
);

Expand All @@ -307,8 +330,13 @@ describe('Gtag wrapping functions', () => {
'gtag'
);
window['dataLayer'] = [];
(window['gtag'] as Gtag)(GtagCommand.SET, { 'language': 'en' });
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
const eventObject = { 'language': 'en' };
(window['gtag'] as Gtag)(GtagCommand.SET, eventObject);
const dataLayer = window['dataLayer'] as DataLayer;
expect(dataLayer.length).to.equal(1);
const data = dataLayer[0];
expect(data[0]).to.equal('set');
expect(data[1]).to.equal(eventObject);
});

it('new window.gtag function does not wait when sending "consent" calls', async () => {
Expand All @@ -329,7 +357,12 @@ describe('Gtag wrapping functions', () => {
'update',
consentParameters
);
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
const dataLayer = window['dataLayer'] as DataLayer;
expect(dataLayer.length).to.equal(1);
const data = dataLayer[0];
expect(data[0]).to.equal('consent');
expect(data[1]).to.equal('update');
expect(data[2]).to.equal(consentParameters);
});

it('new window.gtag function does not wait when sending "get" calls', async () => {
Expand All @@ -347,7 +380,13 @@ describe('Gtag wrapping functions', () => {
'client_id',
clientId => console.log(clientId)
);
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
const dataLayer = window['dataLayer'] as DataLayer;
expect(dataLayer.length).to.equal(1);
const data = dataLayer[0];
expect(data[0]).to.equal('get');
expect(data[1]).to.equal(fakeMeasurementId);
expect(data[2]).to.equal('client_id');
expect(data[3]).to.not.be.undefined;
});

it('new window.gtag function does not wait when sending an unknown command', async () => {
Expand All @@ -360,7 +399,11 @@ describe('Gtag wrapping functions', () => {
);
window['dataLayer'] = [];
(window['gtag'] as Gtag)('new-command-from-gtag-team', fakeMeasurementId);
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
const dataLayer = window['dataLayer'] as DataLayer;
expect(dataLayer.length).to.equal(1);
const data = dataLayer[0];
expect(data[0]).to.equal('new-command-from-gtag-team');
expect(data[1]).to.equal(fakeMeasurementId);
});

it('new window.gtag function waits for initialization promise when sending "config" calls', async () => {
Expand All @@ -373,29 +416,48 @@ describe('Gtag wrapping functions', () => {
'gtag'
);
window['dataLayer'] = [];
(window['gtag'] as Gtag)(GtagCommand.CONFIG, fakeMeasurementId, {
const eventObject = {
'language': 'en'
});
};
(window['gtag'] as Gtag)(
GtagCommand.CONFIG,
fakeMeasurementId,
eventObject
);
expect((window['dataLayer'] as DataLayer).length).to.equal(0);

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

await Promise.all([initPromise1]); // Wait for resolution of Promise.all()

expect((window['dataLayer'] as DataLayer).length).to.equal(1);
const dataLayer = window['dataLayer'] as DataLayer;
expect(dataLayer.length).to.equal(1);
const data = dataLayer[0];
expect(data[0]).to.equal('config');
expect(data[1]).to.equal(fakeMeasurementId);
expect(data[2]).to.equal(eventObject);
});

it('new window.gtag function does not wait when sending "config" calls if there are no pending initialization promises', async () => {
wrapOrCreateGtag({}, fakeDynamicConfigPromises, {}, 'dataLayer', 'gtag');
window['dataLayer'] = [];
(window['gtag'] as Gtag)(GtagCommand.CONFIG, fakeMeasurementId, {
const eventObject = {
'transaction_id': 'abcd123'
});
};
(window['gtag'] as Gtag)(
GtagCommand.CONFIG,
fakeMeasurementId,
eventObject
);
await promiseAllSettled(fakeDynamicConfigPromises);
await Promise.resolve(); // Config call is always chained onto initialization promise list, even if empty.
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
const dataLayer = window['dataLayer'] as DataLayer;
expect(dataLayer.length).to.equal(1);
const data = dataLayer[0];
expect(data[0]).to.equal('config');
expect(data[1]).to.equal(fakeMeasurementId);
expect(data[2]).to.equal(eventObject);
});
});

Expand Down
9 changes: 7 additions & 2 deletions packages/analytics/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,13 @@ function wrapGtag(
gtagParams as GtagConfigOrEventParams
);
} else if (command === GtagCommand.CONSENT) {
const [gtagParams] = args;
gtagCore(GtagCommand.CONSENT, 'update', gtagParams as ConsentSettings);
const [consentAction, gtagParams] = args;
// consentAction can be one of 'default' or 'update'.
gtagCore(
GtagCommand.CONSENT,
consentAction,
gtagParams as ConsentSettings
);
} else if (command === GtagCommand.GET) {
const [measurementId, fieldName, callback] = args;
gtagCore(
Expand Down
3 changes: 2 additions & 1 deletion packages/firestore/src/core/sync_engine_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1095,9 +1095,10 @@ export async function syncEngineEmitNewSnapsAndNotifyLocalStore(
// secondary clients to update query state.
if (viewSnapshot || remoteEvent) {
if (syncEngineImpl.isPrimaryClient) {
const isCurrent = viewSnapshot && !viewSnapshot.fromCache;
syncEngineImpl.sharedClientState.updateQueryState(
queryView.targetId,
viewSnapshot?.fromCache ? 'not-current' : 'current'
isCurrent ? 'current' : 'not-current'
);
}
}
Expand Down
40 changes: 39 additions & 1 deletion packages/firestore/test/unit/specs/query_spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Document } from '../../../src/model/document';
import { doc, filter, query } from '../../util/helpers';

import { describeSpec, specTest } from './describe_spec';
import { spec, SpecBuilder } from './spec_builder';
import { client, spec, SpecBuilder } from './spec_builder';

// Helper to seed the cache with the specified docs by listening to each one.
function specWithCachedDocs(...docs: Document[]): SpecBuilder {
Expand Down Expand Up @@ -136,4 +136,42 @@ describeSpec('Queries:', [], () => {
);
}
);

specTest(
'Queries in different tabs will not interfere',
['multi-client'],
() => {
const query1 = query('collection', filter('key', '==', 'a'));
const query2 = query('collection', filter('key', '==', 'b'));
const docA = doc('collection/a', 1000, { key: 'a' });
const docB = doc('collection/b', 1000, { key: 'b' });

return (
client(0)
.becomeVisible()
// Listen to the first query in the primary client
.expectPrimaryState(true)
.userListens(query1)
.watchAcks(query1)
.watchSends({ affects: [query1] }, docA)

// Listen to different query in the secondary client
.client(1)
.userListens(query2)

.client(0)
.expectListen(query2)
.watchCurrents(query1, 'resume-token-1000')
// Receive global snapshot before the second query is acknowledged
.watchSnapshots(1000)
.expectEvents(query1, { added: [docA] })
// This should not trigger empty snapshot for second query(bugged behavior)
.client(1)
.client(0)
.watchAcksFull(query2, 2000, docB)
.client(1)
.expectEvents(query2, { added: [docB] })
);
}
);
});

0 comments on commit ff65c13

Please sign in to comment.