mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
282 lines
7.5 KiB
TypeScript
282 lines
7.5 KiB
TypeScript
import type {Ref} from 'react';
|
|
import {createContext, Fragment, useEffect, useRef} from 'react';
|
|
import identity from 'lodash/identity';
|
|
|
|
import type {Client} from 'sentry/api';
|
|
import type {Organization} from 'sentry/types';
|
|
import {trackAnalytics} from 'sentry/utils/analytics';
|
|
import useApi from 'sentry/utils/useApi';
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
|
|
import {createDefinedContext} from './utils';
|
|
|
|
type QueryObject = {
|
|
includeAllArgs: boolean | undefined;
|
|
query: {
|
|
[k: string]: any;
|
|
};
|
|
}; // TODO(k-fish): Fix to ensure exact types for all requests. Simplified type for now, need to pull this in from events file.
|
|
|
|
export type Transform = (data: any, queryDefinition: BatchQueryDefinition) => any;
|
|
|
|
type BatchQueryDefinition = {
|
|
api: Client;
|
|
batchProperty: string;
|
|
|
|
path: string;
|
|
reject: (reason?: string) => void;
|
|
requestQueryObject: QueryObject;
|
|
// Intermediate promise functions
|
|
resolve: (value: any) => void;
|
|
transform?: Transform;
|
|
};
|
|
|
|
type QueryBatch = {
|
|
addQuery: (q: BatchQueryDefinition, id: symbol) => void;
|
|
};
|
|
|
|
const [GenericQueryBatcherProvider, _useGenericQueryBatcher] =
|
|
createDefinedContext<QueryBatch>({
|
|
name: 'GenericQueryBatcherContext',
|
|
});
|
|
|
|
function mergeKey(query: BatchQueryDefinition) {
|
|
return `${query.batchProperty}.${query.path}`;
|
|
}
|
|
|
|
type MergeMap = Record<string, BatchQueryDefinition[]>;
|
|
|
|
// Builds a map that will contain an array of query definitions by mergeable key (using batch property and path)
|
|
function queriesToMap(collectedQueries: Record<symbol, BatchQueryDefinition>) {
|
|
const keys = Reflect.ownKeys(collectedQueries);
|
|
if (!keys.length) {
|
|
return false;
|
|
}
|
|
const mergeMap: MergeMap = {};
|
|
|
|
keys.forEach(key => {
|
|
const query = collectedQueries[key];
|
|
mergeMap[mergeKey(query)] = mergeMap[mergeKey(query)] || [];
|
|
mergeMap[mergeKey(query)].push(query);
|
|
delete collectedQueries[key];
|
|
});
|
|
|
|
return mergeMap;
|
|
}
|
|
|
|
function requestFunction(api: Client, path: string, queryObject: QueryObject) {
|
|
return api.requestPromise(path, queryObject);
|
|
}
|
|
|
|
function _handleUnmergeableQuery(queryDefinition: BatchQueryDefinition) {
|
|
const result = requestFunction(
|
|
queryDefinition.api,
|
|
queryDefinition.path,
|
|
queryDefinition.requestQueryObject
|
|
);
|
|
queryDefinition.resolve(result);
|
|
}
|
|
|
|
function _handleUnmergeableQueries(mergeMap: MergeMap) {
|
|
let queriesSent = 0;
|
|
Object.keys(mergeMap).forEach(k => {
|
|
// Using async forEach to ensure calls start in parallel.
|
|
const mergeList = mergeMap[k];
|
|
|
|
if (mergeList.length === 1) {
|
|
const [queryDefinition] = mergeList;
|
|
queriesSent++;
|
|
_handleUnmergeableQuery(queryDefinition);
|
|
}
|
|
});
|
|
|
|
return queriesSent;
|
|
}
|
|
|
|
function _handleMergeableQueries(mergeMap: MergeMap) {
|
|
let queriesSent = 0;
|
|
Object.keys(mergeMap).forEach(async k => {
|
|
const mergeList = mergeMap[k];
|
|
|
|
if (mergeList.length <= 1) {
|
|
return;
|
|
}
|
|
|
|
const [exampleDefinition] = mergeList;
|
|
const batchProperty = exampleDefinition.batchProperty;
|
|
const query = {...exampleDefinition.requestQueryObject.query};
|
|
const requestQueryObject = {...exampleDefinition.requestQueryObject, query};
|
|
|
|
const batchValues: string[] = [];
|
|
|
|
mergeList.forEach(q => {
|
|
const batchFieldValue = q.requestQueryObject.query[batchProperty];
|
|
if (Array.isArray(batchFieldValue)) {
|
|
if (batchFieldValue.length > 1) {
|
|
// Omit multiple requests with multi fields (eg. yAxis) for now and run them as single queries
|
|
queriesSent++;
|
|
_handleUnmergeableQuery(q);
|
|
return;
|
|
}
|
|
// Unwrap array value if it is a single value
|
|
batchValues.push(batchFieldValue[0]);
|
|
} else {
|
|
batchValues.push(batchFieldValue);
|
|
}
|
|
});
|
|
|
|
requestQueryObject.query[batchProperty] = batchValues;
|
|
|
|
queriesSent++;
|
|
const requestPromise = requestFunction(
|
|
exampleDefinition.api,
|
|
exampleDefinition.path,
|
|
requestQueryObject
|
|
);
|
|
|
|
try {
|
|
const result = await requestPromise;
|
|
// Unmerge back into individual results
|
|
mergeList.forEach(queryDefinition => {
|
|
queryDefinition.resolve(
|
|
(queryDefinition.transform || identity)(result, queryDefinition)
|
|
);
|
|
});
|
|
} catch (e) {
|
|
// On error fail all requests relying on this merged query (for now)
|
|
mergeList.forEach(q => q.reject(e));
|
|
}
|
|
});
|
|
return queriesSent;
|
|
}
|
|
|
|
function handleBatching(
|
|
organization: Organization,
|
|
queries: Record<symbol, BatchQueryDefinition>
|
|
) {
|
|
const mergeMap = queriesToMap(queries);
|
|
|
|
if (!mergeMap) {
|
|
return;
|
|
}
|
|
|
|
let queriesSent = 0;
|
|
queriesSent += _handleUnmergeableQueries(mergeMap);
|
|
queriesSent += _handleMergeableQueries(mergeMap);
|
|
|
|
const queriesCollected = Object.values(mergeMap).reduce(
|
|
(acc, mergeList) => acc + mergeList.length,
|
|
0
|
|
);
|
|
|
|
const queriesSaved = queriesCollected - queriesSent;
|
|
|
|
trackAnalytics('performance_views.landingv3.batch_queries', {
|
|
organization,
|
|
num_collected: queriesCollected,
|
|
num_saved: queriesSaved,
|
|
num_sent: queriesSent,
|
|
});
|
|
}
|
|
|
|
export function GenericQueryBatcher({children}: {children: React.ReactNode}) {
|
|
const queries = useRef<Record<symbol, BatchQueryDefinition>>({});
|
|
|
|
const timeoutRef = useRef<number | undefined>(undefined);
|
|
const organization = useOrganization();
|
|
|
|
const addQuery = (q: BatchQueryDefinition, id: symbol) => {
|
|
queries.current[id] = q;
|
|
|
|
window.clearTimeout(timeoutRef.current);
|
|
// Put batch function in the next macro task to aggregate all requests in this frame.
|
|
timeoutRef.current = window.setTimeout(() => {
|
|
handleBatching(organization, queries.current);
|
|
timeoutRef.current = undefined;
|
|
}, 0);
|
|
};
|
|
|
|
// Cleanup timeout after component unmounts.
|
|
useEffect(
|
|
() => () => {
|
|
timeoutRef.current && window.clearTimeout(timeoutRef.current);
|
|
},
|
|
[]
|
|
);
|
|
|
|
return (
|
|
<GenericQueryBatcherProvider
|
|
value={{
|
|
addQuery,
|
|
}}
|
|
>
|
|
{children}
|
|
</GenericQueryBatcherProvider>
|
|
);
|
|
}
|
|
|
|
type NodeContext = {
|
|
batchProperty: string;
|
|
id: Ref<symbol>;
|
|
};
|
|
|
|
const BatchNodeContext = createContext<NodeContext | undefined>(undefined);
|
|
|
|
export type QueryBatching = {
|
|
batchRequest: (_: Client, path: string, query: QueryObject) => Promise<any>;
|
|
};
|
|
|
|
// Wraps api request components to collect at most one request per frame / render pass using symbol as a unique id.
|
|
// Transforms these requests into an intermediate promise and adds a query definition that the batch function will use.
|
|
export function QueryBatchNode(props: {
|
|
batchProperty: string;
|
|
children(_: any): React.ReactNode;
|
|
transform?: Transform;
|
|
}) {
|
|
const api = useApi();
|
|
const {batchProperty, children, transform} = props;
|
|
const id = useRef(Symbol());
|
|
|
|
let batchContext: QueryBatch;
|
|
try {
|
|
batchContext = _useGenericQueryBatcher();
|
|
} catch (_) {
|
|
return <Fragment>{children({})}</Fragment>;
|
|
}
|
|
|
|
function batchRequest(
|
|
_: Client,
|
|
path: string,
|
|
requestQueryObject: QueryObject
|
|
): Promise<any> {
|
|
const queryPromise = new Promise((resolve, reject) => {
|
|
const queryDefinition: BatchQueryDefinition = {
|
|
resolve,
|
|
reject,
|
|
transform,
|
|
batchProperty,
|
|
path,
|
|
requestQueryObject,
|
|
api,
|
|
};
|
|
batchContext?.addQuery(queryDefinition, id.current);
|
|
});
|
|
return queryPromise;
|
|
}
|
|
|
|
const queryBatching: QueryBatching = {
|
|
batchRequest,
|
|
};
|
|
|
|
return (
|
|
<BatchNodeContext.Provider
|
|
value={{
|
|
id,
|
|
batchProperty,
|
|
}}
|
|
>
|
|
{children({queryBatching})}
|
|
</BatchNodeContext.Provider>
|
|
);
|
|
}
|