@parca/profile 0.19.109 → 0.19.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/ProfileSelector/index.d.ts.map +1 -1
  3. package/dist/ProfileSelector/index.js +9 -1
  4. package/dist/ProfileSelector/useAutoQuerySelector.js +1 -1
  5. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts +2 -0
  6. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts.map +1 -1
  7. package/dist/ProfileView/components/ProfileFilters/useProfileFilters.js +3 -1
  8. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts +1 -0
  9. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -1
  10. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +17 -3
  11. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.d.ts +2 -0
  12. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.d.ts.map +1 -0
  13. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.js +541 -0
  14. package/dist/QueryControls/index.d.ts.map +1 -1
  15. package/dist/QueryControls/index.js +1 -3
  16. package/dist/SourceView/Highlighter.d.ts +5 -2
  17. package/dist/SourceView/Highlighter.d.ts.map +1 -1
  18. package/dist/SourceView/Highlighter.js +3 -2
  19. package/dist/SourceView/index.d.ts.map +1 -1
  20. package/dist/SourceView/index.js +40 -11
  21. package/dist/hooks/useLabels.d.ts.map +1 -1
  22. package/dist/hooks/useLabels.js +0 -7
  23. package/dist/hooks/useQueryState.d.ts +4 -0
  24. package/dist/hooks/useQueryState.d.ts.map +1 -1
  25. package/dist/hooks/useQueryState.js +29 -5
  26. package/dist/hooks/useQueryState.test.js +72 -8
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -1
  30. package/package.json +3 -3
  31. package/src/ProfileSelector/index.tsx +14 -1
  32. package/src/ProfileSelector/useAutoQuerySelector.ts +1 -1
  33. package/src/ProfileView/components/ProfileFilters/useProfileFilters.ts +5 -1
  34. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.tsx +663 -0
  35. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +24 -3
  36. package/src/QueryControls/index.tsx +1 -3
  37. package/src/SourceView/Highlighter.tsx +6 -5
  38. package/src/SourceView/index.tsx +45 -12
  39. package/src/hooks/useLabels.ts +0 -10
  40. package/src/hooks/useQueryState.test.tsx +90 -11
  41. package/src/hooks/useQueryState.ts +36 -4
  42. package/src/index.tsx +4 -0
@@ -11,9 +11,9 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {useMemo} from 'react';
14
+ import {useCallback, useMemo} from 'react';
15
15
 
16
- import {useURLStateCustom, type ParamValueSetterCustom} from '@parca/components';
16
+ import {useURLStateBatch, useURLStateCustom, type ParamValueSetterCustom} from '@parca/components';
17
17
  import {safeDecode} from '@parca/utilities';
18
18
 
19
19
  import {isPresetKey} from './filterPresets';
@@ -140,10 +140,13 @@ export const decodeProfileFilters = (encoded: string): ProfileFilter[] => {
140
140
  export const useProfileFiltersUrlState = (): {
141
141
  appliedFilters: ProfileFilter[];
142
142
  setAppliedFilters: ParamValueSetterCustom<ProfileFilter[]>;
143
+ forceApplyFilters: (filters: ProfileFilter[]) => void;
143
144
  } => {
145
+ const batchUpdates = useURLStateBatch();
146
+
144
147
  // Store applied filters in URL state for persistence using compact encoding
145
148
  const [appliedFilters, setAppliedFilters] = useURLStateCustom<ProfileFilter[]>(
146
- 'profile_filters',
149
+ `profile_filters`,
147
150
  {
148
151
  parse: value => {
149
152
  return decodeProfileFilters(value as string);
@@ -159,8 +162,26 @@ export const useProfileFiltersUrlState = (): {
159
162
  return appliedFilters ?? [];
160
163
  }, [appliedFilters]);
161
164
 
165
+ // Force apply filters (bypasses preserve-existing strategy)
166
+ const forceApplyFilters = useCallback(
167
+ (filters: ProfileFilter[]) => {
168
+ const validFilters = filters.filter(f => {
169
+ if (f.type != null && isPresetKey(f.type)) {
170
+ return f.value !== '' && f.type != null;
171
+ }
172
+ return f.value !== '' && f.type != null && f.field != null && f.matchType != null;
173
+ });
174
+
175
+ batchUpdates(() => {
176
+ setAppliedFilters(validFilters);
177
+ });
178
+ },
179
+ [batchUpdates, setAppliedFilters]
180
+ );
181
+
162
182
  return {
163
183
  appliedFilters: memoizedAppliedFilters,
164
184
  setAppliedFilters,
185
+ forceApplyFilters,
165
186
  };
166
187
  };
@@ -177,9 +177,7 @@ export function QueryControls({
177
177
  {viewComponent?.createViewComponent}
178
178
  </div>
179
179
 
180
- {viewComponent?.disableExplorativeQuerying === true &&
181
- viewComponent?.labelnames !== undefined &&
182
- viewComponent?.labelnames.length >= 1 ? (
180
+ {viewComponent?.labelnames !== undefined && viewComponent?.labelnames.length >= 1 ? (
183
181
  <ViewMatchers labelNames={viewComponent.labelnames} />
184
182
  ) : showAdvancedMode && advancedModeForQueryBrowser ? (
185
183
  <MatchersInput
@@ -13,7 +13,6 @@
13
13
 
14
14
  import {MouseEventHandler, useId, useMemo} from 'react';
15
15
 
16
- import {Vector} from 'apache-arrow';
17
16
  import cx from 'classnames';
18
17
  import {scaleLinear} from 'd3-scale';
19
18
  import {type createElementProps} from 'react-syntax-highlighter';
@@ -114,9 +113,10 @@ const charsToWidth = (chars: number): string => {
114
113
  return charsToWidthMap[chars];
115
114
  };
116
115
 
116
+ export type LineDataLookup = (lineNumber: number) => {cumulative: bigint; flat: bigint} | undefined;
117
+
117
118
  export const profileAwareRenderer = (
118
- cumulative: Vector | null,
119
- flat: Vector | null,
119
+ getLineData: LineDataLookup,
120
120
  total: bigint,
121
121
  filtered: bigint,
122
122
  onContextMenu: MouseEventHandler<HTMLDivElement>
@@ -135,6 +135,7 @@ export const profileAwareRenderer = (
135
135
  const lineNumber: number = node.children[0].children[0].value as number;
136
136
  const isCurrentLine = lineNumber >= startLine && lineNumber <= endLine;
137
137
  node.children = node.children.slice(1);
138
+ const data = getLineData(lineNumber);
138
139
  return (
139
140
  <div className="flex gap-1" key={`${i}`}>
140
141
  <div
@@ -161,11 +162,11 @@ export const profileAwareRenderer = (
161
162
  />
162
163
  </div>
163
164
  <LineProfileMetadata
164
- value={cumulative?.get(i) ?? 0n}
165
+ value={data?.cumulative ?? 0n}
165
166
  total={total}
166
167
  filtered={filtered}
167
168
  />
168
- <LineProfileMetadata value={flat?.get(i) ?? 0n} total={total} filtered={filtered} />
169
+ <LineProfileMetadata value={data?.flat ?? 0n} total={total} filtered={filtered} />
169
170
  <div
170
171
  className={cx(
171
172
  'w-full flex-grow-0 border-l border-gray-200 pl-1 dark:border-gray-700',
@@ -22,7 +22,7 @@ import {SourceSkeleton, useParcaContext, useURLState, type ProfileData} from '@p
22
22
 
23
23
  import {ExpandOnHover} from '../GraphTooltipArrow/ExpandOnHoverValue';
24
24
  import {truncateStringReverse} from '../utils';
25
- import {Highlighter, profileAwareRenderer} from './Highlighter';
25
+ import {Highlighter, profileAwareRenderer, type LineDataLookup} from './Highlighter';
26
26
  import useLineRange from './useSelectedLineRange';
27
27
 
28
28
  interface SourceViewProps {
@@ -59,31 +59,64 @@ export const SourceView = React.memo(function SourceView({
59
59
 
60
60
  const {startLine, endLine} = useLineRange();
61
61
 
62
- const [cumulative, flat] = useMemo(() => {
62
+ const sourceTable = useMemo(() => {
63
63
  if (data === undefined) {
64
- return [null, null];
64
+ return null;
65
65
  }
66
66
  const table = tableFromIPC(data.record);
67
- const cumulative = table.getChild('cumulative');
68
- const flat = table.getChild('flat');
69
- return [cumulative, flat];
67
+ return {
68
+ numRows: table.numRows,
69
+ lineNumbers: table.getChild('line_number'),
70
+ cumulative: table.getChild('cumulative'),
71
+ flat: table.getChild('flat'),
72
+ };
70
73
  }, [data]);
71
74
 
75
+ const getLineData: LineDataLookup = useCallback(
76
+ (lineNumber: number) => {
77
+ if (sourceTable === null || sourceTable.lineNumbers === null) {
78
+ return undefined;
79
+ }
80
+ const {numRows, lineNumbers, cumulative, flat} = sourceTable;
81
+
82
+ let lo = 0;
83
+ let hi = numRows - 1;
84
+ while (lo <= hi) {
85
+ const mid = (lo + hi) >>> 1;
86
+ const midVal = Number(lineNumbers.get(mid));
87
+ if (midVal === lineNumber) {
88
+ return {
89
+ cumulative: (cumulative?.get(mid) as bigint) ?? 0n,
90
+ flat: (flat?.get(mid) as bigint) ?? 0n,
91
+ };
92
+ }
93
+ if (midVal < lineNumber) {
94
+ lo = mid + 1;
95
+ } else {
96
+ hi = mid - 1;
97
+ }
98
+ }
99
+ return undefined;
100
+ },
101
+ [sourceTable]
102
+ );
103
+
72
104
  const getProfileDataForLine = useCallback(
73
105
  (line: number, newLine: number): ProfileData | undefined => {
74
- if (cumulative == null && flat == null) {
106
+ const data = getLineData(line);
107
+ if (data === undefined) {
75
108
  return undefined;
76
109
  }
77
- if (cumulative?.get(line - 1) === 0n && flat?.get(line - 1) === 0n) {
110
+ if (data.cumulative === 0n && data.flat === 0n) {
78
111
  return undefined;
79
112
  }
80
113
  return {
81
114
  line: newLine,
82
- cumulative: Number(cumulative?.get(line - 1) ?? 0),
83
- flat: Number(flat?.get(line - 1) ?? 0),
115
+ cumulative: Number(data.cumulative),
116
+ flat: Number(data.flat),
84
117
  };
85
118
  },
86
- [cumulative, flat]
119
+ [getLineData]
87
120
  );
88
121
 
89
122
  const [selectedCode, profileData] = useMemo(() => {
@@ -155,7 +188,7 @@ export const SourceView = React.memo(function SourceView({
155
188
  <Highlighter
156
189
  file={sourceFileName as string}
157
190
  content={data.source}
158
- renderer={profileAwareRenderer(cumulative, flat, total, filtered, onContextMenu)}
191
+ renderer={profileAwareRenderer(getLineData, total, filtered, onContextMenu)}
159
192
  />
160
193
  {sourceViewContextMenuItems.length > 0 ? (
161
194
  <Menu id={MENU_ID}>
@@ -11,8 +11,6 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {useEffect} from 'react';
15
-
16
14
  import {LabelsRequest, LabelsResponse, QueryServiceClient, ValuesRequest} from '@parca/client';
17
15
  import {useGrpcMetadata} from '@parca/components';
18
16
  import {millisToProtoTimestamp, sanitizeLabelValue} from '@parca/utilities';
@@ -70,10 +68,6 @@ export const useLabelNames = (
70
68
  },
71
69
  });
72
70
 
73
- useEffect(() => {
74
- console.log('Label names query result:', {data, error, isLoading});
75
- }, [data, error, isLoading]);
76
-
77
71
  return {
78
72
  result: {response: data, error: error as Error},
79
73
  loading: isLoading,
@@ -113,10 +107,6 @@ export const useLabelValues = (
113
107
  },
114
108
  });
115
109
 
116
- useEffect(() => {
117
- console.log('Label values query result:', {data, error, isLoading, labelName});
118
- }, [data, error, isLoading, labelName]);
119
-
120
110
  return {
121
111
  result: {response: data ?? [], error: error as Error},
122
112
  loading: isLoading,
@@ -11,10 +11,12 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {ReactNode} from 'react';
14
+ import {ReactNode, act} from 'react';
15
15
 
16
16
  // eslint-disable-next-line import/named
17
- import {act, renderHook, waitFor} from '@testing-library/react';
17
+ import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
18
+ // eslint-disable-next-line import/named
19
+ import {renderHook, waitFor} from '@testing-library/react';
18
20
  import {beforeEach, describe, expect, it, vi} from 'vitest';
19
21
 
20
22
  import {URLStateProvider} from '@parca/components';
@@ -99,14 +101,60 @@ vi.mock('../useSumBy', async () => {
99
101
  };
100
102
  });
101
103
 
104
+ // Track profile types loading state for tests
105
+ let mockProfileTypesLoading = false;
106
+ let mockProfileTypesData:
107
+ | {
108
+ types: Array<{
109
+ name: string;
110
+ sampleType: string;
111
+ sampleUnit: string;
112
+ periodType: string;
113
+ periodUnit: string;
114
+ delta: boolean;
115
+ }>;
116
+ }
117
+ | undefined;
118
+
119
+ // Mock useProfileTypes to control loading state in tests
120
+ vi.mock('../ProfileSelector', async () => {
121
+ const actual = await vi.importActual('../ProfileSelector');
122
+ return {
123
+ ...actual,
124
+ useProfileTypes: () => ({
125
+ loading: mockProfileTypesLoading,
126
+ data: mockProfileTypesData,
127
+ error: null,
128
+ }),
129
+ };
130
+ });
131
+
132
+ // Helper to set profile types loading state for tests
133
+ const setProfileTypesLoading = (loading: boolean): void => {
134
+ mockProfileTypesLoading = loading;
135
+ };
136
+
137
+ const setProfileTypesData = (data: typeof mockProfileTypesData): void => {
138
+ mockProfileTypesData = data;
139
+ };
140
+
102
141
  // Helper to create wrapper with URLStateProvider
103
142
  const createWrapper = (
104
143
  paramPreferences = {}
105
144
  ): (({children}: {children: ReactNode}) => JSX.Element) => {
145
+ const queryClient = new QueryClient({
146
+ defaultOptions: {
147
+ queries: {
148
+ retry: false,
149
+ },
150
+ },
151
+ });
106
152
  const Wrapper = ({children}: {children: ReactNode}): JSX.Element => (
107
- <URLStateProvider navigateTo={mockNavigateTo} paramPreferences={paramPreferences}>
108
- {children}
109
- </URLStateProvider>
153
+ <QueryClientProvider client={queryClient}>
154
+ <URLStateProvider navigateTo={mockNavigateTo} paramPreferences={paramPreferences}>
155
+ {children}
156
+ </URLStateProvider>
157
+ </QueryClientProvider>
110
158
  );
111
159
  Wrapper.displayName = 'URLStateProviderWrapper';
112
160
  return Wrapper;
@@ -120,6 +168,9 @@ describe('useQueryState', () => {
120
168
  writable: true,
121
169
  });
122
170
  mockLocation.search = '';
171
+ // Reset profile types mock state
172
+ setProfileTypesLoading(false);
173
+ setProfileTypesData(undefined);
123
174
  });
124
175
 
125
176
  describe('Basic functionality', () => {
@@ -127,7 +178,7 @@ describe('useQueryState', () => {
127
178
  const {result} = renderHook(
128
179
  () =>
129
180
  useQueryState({
130
- defaultExpression: 'process_cpu{}',
181
+ defaultExpression: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{}',
131
182
  defaultTimeSelection: 'relative:hour|1',
132
183
  defaultFrom: 1000,
133
184
  defaultTo: 2000,
@@ -136,7 +187,7 @@ describe('useQueryState', () => {
136
187
  );
137
188
 
138
189
  const {querySelection} = result.current;
139
- expect(querySelection.expression).toBe('process_cpu{}');
190
+ expect(querySelection.expression).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{}');
140
191
  expect(querySelection.timeSelection).toBe('relative:hour|1');
141
192
  // From/to should be calculated from the range
142
193
  expect(querySelection.from).toBeDefined();
@@ -524,7 +575,9 @@ describe('useQueryState', () => {
524
575
  });
525
576
 
526
577
  describe('Edge cases', () => {
527
- it('should handle invalid expression gracefully', () => {
578
+ it('should handle invalid expression gracefully and log warning', () => {
579
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
580
+
528
581
  const {result} = renderHook(
529
582
  () =>
530
583
  useQueryState({
@@ -533,8 +586,33 @@ describe('useQueryState', () => {
533
586
  {wrapper: createWrapper()}
534
587
  );
535
588
 
536
- // Should not throw error
589
+ // Should not throw error - invalid expressions are caught and logged
590
+ expect(() => result.current.querySelection).not.toThrow();
591
+ // Should fall back to empty expression
592
+ expect(result.current.querySelection.expression).toBe('invalid{{}expression');
593
+ // Should log a warning about the parse failure
594
+ expect(consoleSpy).toHaveBeenCalledWith(
595
+ 'Failed to parse expression',
596
+ expect.objectContaining({
597
+ expression: 'invalid{{}expression',
598
+ })
599
+ );
600
+
601
+ consoleSpy.mockRestore();
602
+ });
603
+
604
+ it('should handle empty expression gracefully', () => {
605
+ const {result} = renderHook(
606
+ () =>
607
+ useQueryState({
608
+ defaultExpression: '',
609
+ }),
610
+ {wrapper: createWrapper()}
611
+ );
612
+
613
+ // Should not throw error with empty expression
537
614
  expect(() => result.current.querySelection).not.toThrow();
615
+ expect(result.current.querySelection.expression).toBe('');
538
616
  });
539
617
 
540
618
  it('should clear merge params for non-delta profiles', async () => {
@@ -1191,7 +1269,8 @@ describe('useQueryState', () => {
1191
1269
  });
1192
1270
 
1193
1271
  it('should preserve other URL params when setting ProfileSelection', async () => {
1194
- mockLocation.search = '?expression_a=process_cpu{}&other_param=value&unrelated=test';
1272
+ mockLocation.search =
1273
+ '?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&other_param=value&unrelated=test';
1195
1274
 
1196
1275
  const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
1197
1276
 
@@ -1212,7 +1291,7 @@ describe('useQueryState', () => {
1212
1291
  expect(params.selection_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}');
1213
1292
 
1214
1293
  // Other params should be preserved
1215
- expect(params.expression_a).toBe('process_cpu{}');
1294
+ expect(params.expression_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{}');
1216
1295
  expect(params.other_param).toBe('value');
1217
1296
  expect(params.unrelated).toBe('test');
1218
1297
  });
@@ -29,6 +29,7 @@ interface UseQueryStateOptions {
29
29
  defaultFrom?: number;
30
30
  defaultTo?: number;
31
31
  comparing?: boolean; // If true, don't auto-select for delta profiles
32
+ onProfileTypeChange?: () => void; // Called when profile type changes on commit, after reset
32
33
  }
33
34
 
34
35
  interface UseQueryStateReturn {
@@ -65,6 +66,10 @@ interface UseQueryStateReturn {
65
66
 
66
67
  // parsed query
67
68
  parsedQuery: Query | null;
69
+
70
+ setExpressionParam: (value: string | undefined) => void;
71
+ setSumByParam: (value: string | undefined) => void;
72
+ setGroupByParam: (value: string[] | undefined) => void;
68
73
  }
69
74
 
70
75
  export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryStateReturn => {
@@ -76,6 +81,7 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
76
81
  defaultFrom,
77
82
  defaultTo,
78
83
  comparing = false,
84
+ onProfileTypeChange,
79
85
  } = options;
80
86
 
81
87
  const batchUpdates = useURLStateBatch();
@@ -101,6 +107,10 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
101
107
 
102
108
  const [sumByParam, setSumByParam] = useURLState<string>(`sum_by${suffix}`);
103
109
 
110
+ const [, setGroupByParam] = useURLState<string>('group_by', {
111
+ alwaysReturnArray: true,
112
+ });
113
+
104
114
  const [mergeFrom, setMergeFromState] = useURLState<string>(`merge_from${suffix}`);
105
115
  const [mergeTo, setMergeToState] = useURLState<string>(`merge_to${suffix}`);
106
116
 
@@ -121,7 +131,11 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
121
131
  const draftQuery = useMemo(() => {
122
132
  try {
123
133
  return Query.parse(draftExpression ?? '');
124
- } catch {
134
+ } catch (error) {
135
+ console.warn('Failed to parse draft expression', {
136
+ expression: draftExpression,
137
+ error: error instanceof Error ? error.message : String(error),
138
+ });
125
139
  return Query.parse('');
126
140
  }
127
141
  }, [draftExpression]);
@@ -129,7 +143,11 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
129
143
  const query = useMemo(() => {
130
144
  try {
131
145
  return Query.parse(expression ?? '');
132
- } catch {
146
+ } catch (error) {
147
+ console.warn('Failed to parse expression', {
148
+ expression,
149
+ error: error instanceof Error ? error.message : String(error),
150
+ });
133
151
  return Query.parse('');
134
152
  }
135
153
  }, [expression]);
@@ -346,6 +364,7 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
346
364
  Query.parse(querySelection.expression).profileType().toString()
347
365
  ) {
348
366
  resetStateOnProfileTypeChange();
367
+ onProfileTypeChange?.();
349
368
  }
350
369
  });
351
370
  },
@@ -369,6 +388,7 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
369
388
  setSelectionParam,
370
389
  resetFlameGraphState,
371
390
  resetStateOnProfileTypeChange,
391
+ onProfileTypeChange,
372
392
  draftProfileType,
373
393
  querySelection.expression,
374
394
  ]
@@ -426,7 +446,11 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
426
446
  const draftParsedQuery = useMemo(() => {
427
447
  try {
428
448
  return Query.parse(draftSelection.expression ?? '');
429
- } catch {
449
+ } catch (error) {
450
+ console.warn('Failed to parse draft selection expression', {
451
+ expression: draftSelection.expression,
452
+ error: error instanceof Error ? error.message : String(error),
453
+ });
430
454
  return Query.parse('');
431
455
  }
432
456
  }, [draftSelection.expression]);
@@ -434,7 +458,11 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
434
458
  const parsedQuery = useMemo(() => {
435
459
  try {
436
460
  return Query.parse(querySelection.expression ?? '');
437
- } catch {
461
+ } catch (error) {
462
+ console.warn('Failed to parse query selection expression', {
463
+ expression: querySelection.expression,
464
+ error: error instanceof Error ? error.message : String(error),
465
+ });
438
466
  return Query.parse('');
439
467
  }
440
468
  }, [querySelection.expression]);
@@ -466,5 +494,9 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
466
494
 
467
495
  draftParsedQuery,
468
496
  parsedQuery,
497
+
498
+ setExpressionParam: setExpressionState,
499
+ setSumByParam,
500
+ setGroupByParam,
469
501
  };
470
502
  };
package/src/index.tsx CHANGED
@@ -34,6 +34,8 @@ export * from './ProfileSource';
34
34
  export {
35
35
  convertToProtoFilters,
36
36
  convertFromProtoFilters,
37
+ useProfileFilters,
38
+ type ProfileFilter,
37
39
  } from './ProfileView/components/ProfileFilters/useProfileFilters';
38
40
  export * from './ProfileView';
39
41
  export * from './ProfileViewWithData';
@@ -57,6 +59,8 @@ export const DEFAULT_PROFILE_EXPLORER_PARAM_VALUES: ParamPreferences = {
57
59
  },
58
60
  };
59
61
 
62
+ export {useProfileTypes} from './ProfileSelector';
63
+
60
64
  export {
61
65
  ProfileExplorer,
62
66
  ProfileTypeSelector,