Skip to content

Commit ff2539a

Browse files
scttcperandrewshie-sentry
authored andcommitted
feat(bootstrap): Cache projects in IndexedDB (#89139)
Implements the caching part of https://www.notion.so/sentry/Tech-Spec-Frontend-Bootstrap-Cache-Project-Loading-Performance-1748b10e4b5d80759127c15d3772e063 Went with IndexedDB because some organizations have multiple megabytes of projects to cache. This helps bandaid the entire app waiting for projects to load by storing them in the cache and revalidating.
1 parent 575e2a2 commit ff2539a

File tree

6 files changed

+139
-28
lines changed

6 files changed

+139
-28
lines changed

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@
6464
"@sentry/status-page-list": "^0.6.0",
6565
"@sentry/webpack-plugin": "^3.1.1",
6666
"@spotlightjs/spotlight": "^2.0.0-alpha.1",
67-
"@tanstack/react-query": "^5.64.1",
68-
"@tanstack/react-query-devtools": "^5.64.1",
67+
"@tanstack/query-async-storage-persister": "^5.72.1",
68+
"@tanstack/react-query": "^5.72.1",
69+
"@tanstack/react-query-devtools": "^5.72.1",
70+
"@tanstack/react-query-persist-client": "^5.72.1",
6971
"@tanstack/react-virtual": "^3.5.1",
7072
"@types/color": "^3.0.3",
7173
"@types/diff": "5.2.1",
@@ -117,6 +119,7 @@
117119
"fuse.js": "^6.6.2",
118120
"gettext-parser": "1.3.1",
119121
"gl-matrix": "^3.4.3",
122+
"idb-keyval": "^6.2.1",
120123
"invariant": "^2.2.4",
121124
"jed": "^1.1.0",
122125
"jest-fetch-mock": "^3.0.3",

static/app/appQueryClient.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister';
2+
import {persistQueryClient} from '@tanstack/react-query-persist-client';
3+
import {del as removeItem, get as getItem, set as setItem} from 'idb-keyval';
4+
5+
import {SENTRY_RELEASE_VERSION} from 'sentry/constants';
6+
import {DEFAULT_QUERY_CLIENT_CONFIG, QueryClient} from 'sentry/utils/queryClient';
7+
8+
/**
9+
* Named it appQueryClient because we already have a queryClient in sentry/utils/queryClient
10+
* sentry/utils/queryClient is a small wrapper around react-query's functionality for our API.
11+
*
12+
* appQueryClient below is the app's react-query cache and should not be imported directly.
13+
* Instead, use `const queryClient = useQueryClient()`.
14+
* @link https://tanstack.com/query/v5/docs/reference/QueryClient
15+
*/
16+
export const appQueryClient = new QueryClient(DEFAULT_QUERY_CLIENT_CONFIG);
17+
const cacheKey = 'sentry-react-query-cache';
18+
19+
const localStoragePersister = createAsyncStoragePersister({
20+
// We're using indexedDB as our storage provider because projects cache can be large
21+
storage: {getItem, setItem, removeItem},
22+
// Reduce the frequency of writes to indexedDB
23+
throttleTime: 10_000,
24+
// The cache is stored entirely on one key
25+
key: cacheKey,
26+
});
27+
28+
const isProjectsCacheEnabled =
29+
window.indexedDB &&
30+
(window.__initialData?.features as unknown as string[])?.includes(
31+
'organizations:cache-projects-ui'
32+
);
33+
34+
/**
35+
* Attach the persister to the query client
36+
* @link https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient
37+
*/
38+
if (isProjectsCacheEnabled) {
39+
persistQueryClient({
40+
queryClient: appQueryClient,
41+
persister: localStoragePersister,
42+
/**
43+
* Clear cache on release version change
44+
* Locally this does nothing, if you need to clear cache locally you can clear indexdb
45+
*/
46+
buster: SENTRY_RELEASE_VERSION ?? 'local',
47+
dehydrateOptions: {
48+
// Persist a subset of queries to local storage
49+
shouldDehydrateQuery(query) {
50+
// This could be extended later to persist other queries
51+
return (
52+
// Query is not pending or failed
53+
query.state.status === 'success' &&
54+
!query.isStale() &&
55+
Array.isArray(query.queryKey) &&
56+
// Currently only bootstrap-projects is persisted
57+
query.queryKey[0] === 'bootstrap-projects'
58+
);
59+
},
60+
},
61+
});
62+
}
63+
64+
export function restoreQueryCache() {
65+
if (isProjectsCacheEnabled) {
66+
localStoragePersister.restoreClient();
67+
}
68+
}
69+
70+
export async function clearQueryCache() {
71+
if (isProjectsCacheEnabled) {
72+
// Mark queries as stale so they won't be recached
73+
appQueryClient.invalidateQueries({queryKey: ['bootstrap-projects']});
74+
await removeItem(cacheKey);
75+
}
76+
}

static/app/bootstrap/initializeApp.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import './legacyTwitterBootstrap';
22
import './exportGlobals';
33

4+
import {restoreQueryCache} from 'sentry/appQueryClient';
45
import type {Config} from 'sentry/types/system';
56
import {metric} from 'sentry/utils/analytics';
67

@@ -11,6 +12,7 @@ import {renderMain} from './renderMain';
1112
import {renderOnDomReady} from './renderOnDomReady';
1213

1314
export function initializeApp(config: Config) {
15+
restoreQueryCache();
1416
commonInitialization(config);
1517
initializeSdk(config);
1618

static/app/main.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,16 @@ import {createBrowserRouter, RouterProvider} from 'react-router-dom';
33
import {wrapCreateBrowserRouterV6} from '@sentry/react';
44
import {ReactQueryDevtools} from '@tanstack/react-query-devtools';
55

6+
import {appQueryClient} from 'sentry/appQueryClient';
67
import {OnboardingContextProvider} from 'sentry/components/onboarding/onboardingContext';
78
import {ThemeAndStyleProvider} from 'sentry/components/themeAndStyleProvider';
89
import {USE_REACT_QUERY_DEVTOOL} from 'sentry/constants';
910
import {routes} from 'sentry/routes';
1011
import {DANGEROUS_SET_REACT_ROUTER_6_HISTORY} from 'sentry/utils/browserHistory';
11-
import {
12-
DEFAULT_QUERY_CLIENT_CONFIG,
13-
QueryClient,
14-
QueryClientProvider,
15-
} from 'sentry/utils/queryClient';
12+
import {QueryClientProvider} from 'sentry/utils/queryClient';
1613

1714
import {buildReactRouter6Routes} from './utils/reactRouter6Compat/router';
1815

19-
const queryClient = new QueryClient(DEFAULT_QUERY_CLIENT_CONFIG);
20-
2116
function buildRouter() {
2217
const sentryCreateBrowserRouter = wrapCreateBrowserRouterV6(createBrowserRouter);
2318
const router = sentryCreateBrowserRouter(buildReactRouter6Routes(routes()));
@@ -30,7 +25,7 @@ function Main() {
3025
const [router] = useState(buildRouter);
3126

3227
return (
33-
<QueryClientProvider client={queryClient}>
28+
<QueryClientProvider client={appQueryClient}>
3429
<ThemeAndStyleProvider>
3530
<OnboardingContextProvider>
3631
<RouterProvider router={router} />

static/app/stores/projectsStore.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {createStore} from 'reflux';
22

33
import {fetchOrganizationDetails} from 'sentry/actionCreators/organization';
44
import {Client} from 'sentry/api';
5+
import {clearQueryCache} from 'sentry/appQueryClient';
56
import type {Team} from 'sentry/types/organization';
67
import type {Project} from 'sentry/types/project';
78

@@ -84,6 +85,7 @@ const storeConfig: ProjectsStoreDefinition = {
8485
this.state = {...this.state, projects: newProjects};
8586

8687
this.trigger(new Set([prevProject.id]));
88+
clearQueryCache();
8789
},
8890

8991
onCreateSuccess(project: Project, orgSlug: string) {
@@ -94,6 +96,7 @@ const storeConfig: ProjectsStoreDefinition = {
9496

9597
// Reload organization details since we've created a new project
9698
fetchOrganizationDetails(this.api, orgSlug);
99+
clearQueryCache();
97100

98101
this.trigger(new Set([project.id]));
99102
},
@@ -112,6 +115,7 @@ const storeConfig: ProjectsStoreDefinition = {
112115
this.state = {...this.state, projects: newProjects};
113116

114117
this.trigger(new Set([data.id]));
118+
clearQueryCache();
115119
},
116120

117121
onStatsLoadSuccess(data) {
@@ -134,6 +138,7 @@ const storeConfig: ProjectsStoreDefinition = {
134138
const newProjects = this.state.projects.filter(p => p.id !== project.id);
135139
this.state = {...this.state, projects: newProjects};
136140
this.trigger(new Set([project.id]));
141+
clearQueryCache();
137142
},
138143

139144
/**
@@ -151,6 +156,7 @@ const storeConfig: ProjectsStoreDefinition = {
151156

152157
const affectedProjectIds = projects.map(project => project.id);
153158
this.trigger(new Set(affectedProjectIds));
159+
clearQueryCache();
154160
},
155161

156162
onRemoveTeam(teamSlug: string, projectSlug: string) {
@@ -162,6 +168,7 @@ const storeConfig: ProjectsStoreDefinition = {
162168

163169
this.removeTeamFromProject(teamSlug, project);
164170
this.trigger(new Set([project.id]));
171+
clearQueryCache();
165172
},
166173

167174
onAddTeam(team: Team, projectSlug: string) {
@@ -179,6 +186,7 @@ const storeConfig: ProjectsStoreDefinition = {
179186
this.state = {...this.state, projects: newProjects};
180187

181188
this.trigger(new Set([project.id]));
189+
clearQueryCache();
182190
},
183191

184192
// Internal method, does not trigger
@@ -189,6 +197,7 @@ const storeConfig: ProjectsStoreDefinition = {
189197
p.id === project.id ? newProject : p
190198
);
191199
this.state = {...this.state, projects: newProjects};
200+
clearQueryCache();
192201
},
193202

194203
isLoading() {

yarn.lock

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3624,29 +3624,50 @@
36243624
dependencies:
36253625
"@typescript-eslint/utils" "^8.18.1"
36263626

3627-
"@tanstack/query-core@5.64.1":
3628-
version "5.64.1"
3629-
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.64.1.tgz#d56e26b3e29fc68a89d140f1fd92900bc8f3fc86"
3630-
integrity sha512-978Wx4Wl4UJZbmvU/rkaM9cQtXXrbhK0lsz/UZhYIbyKYA8E4LdomTwyh2GHZ4oU0BKKoDH4YlKk2VscCUgNmg==
3627+
"@tanstack/query-async-storage-persister@^5.72.1":
3628+
version "5.72.1"
3629+
resolved "https://registry.yarnpkg.com/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.72.1.tgz#2d4c230efbab7670fa03d483196232ff34a465c1"
3630+
integrity sha512-5l6NuV1GMz0Y2qGJ9B1JYgpDmLb92fs4c56/cxgNACwwi+bWyzEySQZocyXxiEW4ZG8fSQ/la57nAAw+OSsp0w==
3631+
dependencies:
3632+
"@tanstack/query-persist-client-core" "5.72.1"
3633+
3634+
"@tanstack/query-core@5.72.1":
3635+
version "5.72.1"
3636+
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.72.1.tgz#c46e3e5eecf1628c478965c0540cae98838e24fe"
3637+
integrity sha512-nOu0EEkZuJ0BZnYgeaEfo44+psq1jBO7/zp3KudixD4dvgOVerrhAhDEKsWx2N7MxB59mjO4r0ddP/VqWGPK+Q==
3638+
3639+
"@tanstack/query-devtools@5.72.1":
3640+
version "5.72.1"
3641+
resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.72.1.tgz#3ef35de317a55a688072a13cb628931ddec0f097"
3642+
integrity sha512-D0vEoQaiVq9ayCqvvxA9XkDq7TIesyPpvgP69arRtt5FQF6n/Hrta4SlkfXC4m9BCvFLlhLDcKGYa2eMQ4ZIIA==
3643+
3644+
"@tanstack/query-persist-client-core@5.72.1":
3645+
version "5.72.1"
3646+
resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.72.1.tgz#65ba35375677723fc51d28295cdaddfb20d65855"
3647+
integrity sha512-sceggk1lnJVNlvAUFoWZgaC1SVPgxyhoShHGczlAZHZIn7uLkEJcICXUQlvTSeJn4nr8XdFxBSW8ie/6YPmr6A==
3648+
dependencies:
3649+
"@tanstack/query-core" "5.72.1"
36313650

3632-
"@tanstack/query-devtools@5.62.16":
3633-
version "5.62.16"
3634-
resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.62.16.tgz#a4b71c6b5bbf7575861437ef9a9f232333569255"
3635-
integrity sha512-3ff6UBJr0H3nIhfLSl9911rvKqXf0u4B58jl0uYdDWLqPk9pCvYIbxC35cGxK2+8INl4IaFVUHb/IdgWrNkg3Q==
3651+
"@tanstack/react-query-devtools@^5.72.1":
3652+
version "5.72.1"
3653+
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.72.1.tgz#9822a0799125dc67ff1bd846cf01a524b12fa998"
3654+
integrity sha512-ckNRgABst3MLjpM2nD/CzQToCiaT3jb3Xhtf+GP/0/9ij9SPT/SC+lc3wUDSkT0OupnHobBBF5E1/Xp6B+XZLg==
3655+
dependencies:
3656+
"@tanstack/query-devtools" "5.72.1"
36363657

3637-
"@tanstack/react-query-devtools@^5.64.1":
3638-
version "5.64.1"
3639-
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.64.1.tgz#e3ccf56b1b30453a4baed2b05d4fa717885ddf97"
3640-
integrity sha512-8ajcGE3wXYlb4KuJnkFYkJwJKc/qmPNTpQD7YTgLRMBPTGGp1xk7VMzxL87DoXuweO8luplUUblJJ3noVs/luQ==
3658+
"@tanstack/react-query-persist-client@^5.72.1":
3659+
version "5.72.1"
3660+
resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.72.1.tgz#9564de508fb25ef1d63430b6ad3cea3f9727f28a"
3661+
integrity sha512-VJ1wqgfDblyjjxdrm910bQflHNC6IVcqKMN6/DIhC7AGw8jeSKWDc7AVZW4d9L57tblyYdQfUfwTOBfpGk646Q==
36413662
dependencies:
3642-
"@tanstack/query-devtools" "5.62.16"
3663+
"@tanstack/query-persist-client-core" "5.72.1"
36433664

3644-
"@tanstack/react-query@^5.64.1":
3645-
version "5.64.1"
3646-
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.64.1.tgz#46b4182f5b045299e4be8d0a91c549ac5dc0a20c"
3647-
integrity sha512-vW5ggHpIO2Yjj44b4sB+Fd3cdnlMJppXRBJkEHvld6FXh3j5dwWJoQo7mGtKI2RbSFyiyu/PhGAy0+Vv5ev9Eg==
3665+
"@tanstack/react-query@^5.72.1":
3666+
version "5.72.1"
3667+
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.72.1.tgz#7c8bcb94bfc64e57377bfa3c5b2a764c4ae83229"
3668+
integrity sha512-4UEMyRx54xj144D2nDvDIMiXSG5BrqyCJrmyNoGbymNS+VWODcBDFrmRk9p2fe12UGZ4JtKPTNuW2Jg0aisUgQ==
36483669
dependencies:
3649-
"@tanstack/query-core" "5.64.1"
3670+
"@tanstack/query-core" "5.72.1"
36503671

36513672
"@tanstack/react-virtual@^3.5.1":
36523673
version "3.5.1"
@@ -7719,6 +7740,11 @@ icss-utils@^5.0.0, icss-utils@^5.1.0:
77197740
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
77207741
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
77217742

7743+
idb-keyval@^6.2.1:
7744+
version "6.2.1"
7745+
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33"
7746+
integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==
7747+
77227748
ieee754@^1.2.1:
77237749
version "1.2.1"
77247750
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"

0 commit comments

Comments
 (0)