Skip to content

Commit 0cbd4f3

Browse files
committed
feat(bootstrap): Cache projects in IndexedDB
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 7002ae7 commit 0cbd4f3

File tree

6 files changed

+128
-28
lines changed

6 files changed

+128
-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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
* @link https://tanstack.com/query/v5/docs/reference/QueryClient
11+
*/
12+
export const appQueryClient = new QueryClient(DEFAULT_QUERY_CLIENT_CONFIG);
13+
const cacheKey = 'sentry-react-query-cache';
14+
15+
const localStoragePersister = createAsyncStoragePersister({
16+
// We're using indexedDB as our storage provider because projects cache can be large
17+
storage: {getItem, setItem, removeItem},
18+
// Reduce the frequency of writes to indexedDB
19+
throttleTime: 15_000,
20+
// The cache is stored entirely on one key
21+
key: cacheKey,
22+
});
23+
24+
const isProjectsCacheEnabled = (
25+
window.__initialData?.features as unknown as string[]
26+
)?.includes('organizations:cache-projects-ui');
27+
28+
/**
29+
* Attach the persister to the query client
30+
* @link https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient
31+
*/
32+
if (isProjectsCacheEnabled) {
33+
persistQueryClient({
34+
queryClient: appQueryClient,
35+
persister: localStoragePersister,
36+
/**
37+
* Clear cache on release version change
38+
* Locally this does nothing, if you need to clear cache locally you can clear indexdb
39+
*/
40+
buster: SENTRY_RELEASE_VERSION ?? 'local',
41+
dehydrateOptions: {
42+
// Persist a subset of queries to local storage
43+
shouldDehydrateQuery(query) {
44+
// This could be extended later to persist other queries
45+
return (
46+
Array.isArray(query.queryKey) &&
47+
typeof query.queryKey[0] === 'string' &&
48+
// Only persist queryKey bootstrap-projects for now
49+
query.queryKey[0] === 'bootstrap-projects'
50+
);
51+
},
52+
},
53+
});
54+
}
55+
56+
export function restoreQueryCache() {
57+
if (isProjectsCacheEnabled) {
58+
localStoragePersister.restoreClient();
59+
}
60+
}
61+
62+
export async function clearQueryCache() {
63+
await localStoragePersister.removeClient();
64+
await removeItem(cacheKey);
65+
}

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
@@ -3634,29 +3634,50 @@
36343634
dependencies:
36353635
"@typescript-eslint/utils" "^8.18.1"
36363636

3637-
"@tanstack/query-core@5.64.1":
3638-
version "5.64.1"
3639-
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.64.1.tgz#d56e26b3e29fc68a89d140f1fd92900bc8f3fc86"
3640-
integrity sha512-978Wx4Wl4UJZbmvU/rkaM9cQtXXrbhK0lsz/UZhYIbyKYA8E4LdomTwyh2GHZ4oU0BKKoDH4YlKk2VscCUgNmg==
3637+
"@tanstack/query-async-storage-persister@^5.72.1":
3638+
version "5.72.1"
3639+
resolved "https://registry.yarnpkg.com/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.72.1.tgz#2d4c230efbab7670fa03d483196232ff34a465c1"
3640+
integrity sha512-5l6NuV1GMz0Y2qGJ9B1JYgpDmLb92fs4c56/cxgNACwwi+bWyzEySQZocyXxiEW4ZG8fSQ/la57nAAw+OSsp0w==
3641+
dependencies:
3642+
"@tanstack/query-persist-client-core" "5.72.1"
3643+
3644+
"@tanstack/query-core@5.72.1":
3645+
version "5.72.1"
3646+
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.72.1.tgz#c46e3e5eecf1628c478965c0540cae98838e24fe"
3647+
integrity sha512-nOu0EEkZuJ0BZnYgeaEfo44+psq1jBO7/zp3KudixD4dvgOVerrhAhDEKsWx2N7MxB59mjO4r0ddP/VqWGPK+Q==
3648+
3649+
"@tanstack/query-devtools@5.72.1":
3650+
version "5.72.1"
3651+
resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.72.1.tgz#3ef35de317a55a688072a13cb628931ddec0f097"
3652+
integrity sha512-D0vEoQaiVq9ayCqvvxA9XkDq7TIesyPpvgP69arRtt5FQF6n/Hrta4SlkfXC4m9BCvFLlhLDcKGYa2eMQ4ZIIA==
3653+
3654+
"@tanstack/query-persist-client-core@5.72.1":
3655+
version "5.72.1"
3656+
resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.72.1.tgz#65ba35375677723fc51d28295cdaddfb20d65855"
3657+
integrity sha512-sceggk1lnJVNlvAUFoWZgaC1SVPgxyhoShHGczlAZHZIn7uLkEJcICXUQlvTSeJn4nr8XdFxBSW8ie/6YPmr6A==
3658+
dependencies:
3659+
"@tanstack/query-core" "5.72.1"
36413660

3642-
"@tanstack/query-devtools@5.62.16":
3643-
version "5.62.16"
3644-
resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.62.16.tgz#a4b71c6b5bbf7575861437ef9a9f232333569255"
3645-
integrity sha512-3ff6UBJr0H3nIhfLSl9911rvKqXf0u4B58jl0uYdDWLqPk9pCvYIbxC35cGxK2+8INl4IaFVUHb/IdgWrNkg3Q==
3661+
"@tanstack/react-query-devtools@^5.72.1":
3662+
version "5.72.1"
3663+
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.72.1.tgz#9822a0799125dc67ff1bd846cf01a524b12fa998"
3664+
integrity sha512-ckNRgABst3MLjpM2nD/CzQToCiaT3jb3Xhtf+GP/0/9ij9SPT/SC+lc3wUDSkT0OupnHobBBF5E1/Xp6B+XZLg==
3665+
dependencies:
3666+
"@tanstack/query-devtools" "5.72.1"
36463667

3647-
"@tanstack/react-query-devtools@^5.64.1":
3648-
version "5.64.1"
3649-
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.64.1.tgz#e3ccf56b1b30453a4baed2b05d4fa717885ddf97"
3650-
integrity sha512-8ajcGE3wXYlb4KuJnkFYkJwJKc/qmPNTpQD7YTgLRMBPTGGp1xk7VMzxL87DoXuweO8luplUUblJJ3noVs/luQ==
3668+
"@tanstack/react-query-persist-client@^5.72.1":
3669+
version "5.72.1"
3670+
resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-5.72.1.tgz#9564de508fb25ef1d63430b6ad3cea3f9727f28a"
3671+
integrity sha512-VJ1wqgfDblyjjxdrm910bQflHNC6IVcqKMN6/DIhC7AGw8jeSKWDc7AVZW4d9L57tblyYdQfUfwTOBfpGk646Q==
36513672
dependencies:
3652-
"@tanstack/query-devtools" "5.62.16"
3673+
"@tanstack/query-persist-client-core" "5.72.1"
36533674

3654-
"@tanstack/react-query@^5.64.1":
3655-
version "5.64.1"
3656-
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.64.1.tgz#46b4182f5b045299e4be8d0a91c549ac5dc0a20c"
3657-
integrity sha512-vW5ggHpIO2Yjj44b4sB+Fd3cdnlMJppXRBJkEHvld6FXh3j5dwWJoQo7mGtKI2RbSFyiyu/PhGAy0+Vv5ev9Eg==
3675+
"@tanstack/react-query@^5.72.1":
3676+
version "5.72.1"
3677+
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.72.1.tgz#7c8bcb94bfc64e57377bfa3c5b2a764c4ae83229"
3678+
integrity sha512-4UEMyRx54xj144D2nDvDIMiXSG5BrqyCJrmyNoGbymNS+VWODcBDFrmRk9p2fe12UGZ4JtKPTNuW2Jg0aisUgQ==
36583679
dependencies:
3659-
"@tanstack/query-core" "5.64.1"
3680+
"@tanstack/query-core" "5.72.1"
36603681

36613682
"@tanstack/react-virtual@^3.5.1":
36623683
version "3.5.1"
@@ -7754,6 +7775,11 @@ icss-utils@^5.0.0, icss-utils@^5.1.0:
77547775
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
77557776
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
77567777

7778+
idb-keyval@^6.2.1:
7779+
version "6.2.1"
7780+
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33"
7781+
integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==
7782+
77577783
ieee754@^1.2.1:
77587784
version "1.2.1"
77597785
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"

0 commit comments

Comments
 (0)