@atlaskit/editor-plugin-autocomplete 2.3.0 → 2.5.0

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 (66) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/afm-cc/tsconfig.json +3 -0
  3. package/afm-products/tsconfig.json +3 -0
  4. package/autocompletePlugin/package.json +15 -0
  5. package/autocompletePluginType/package.json +15 -0
  6. package/dist/cjs/analytics/ufo.js +111 -0
  7. package/dist/cjs/entry-points/autocompletePlugin.js +12 -0
  8. package/dist/cjs/entry-points/autocompletePluginType.js +1 -0
  9. package/dist/cjs/entry-points/src-pm-plugins-autocomplete-plugin.js +18 -0
  10. package/dist/cjs/entry-points/src-pm-plugins-slow-lane-client.js +36 -0
  11. package/dist/cjs/entry-points/src-pm-plugins-text-predictor.js +66 -0
  12. package/dist/cjs/pm-plugins/autocomplete-plugin.js +126 -64
  13. package/dist/cjs/pm-plugins/local-slow-lane-client.js +447 -0
  14. package/dist/cjs/pm-plugins/slow-lane-client.js +45 -16
  15. package/dist/cjs/pm-plugins/text-predictor.js +72 -40
  16. package/dist/es2019/analytics/ufo.js +110 -0
  17. package/dist/es2019/entry-points/autocompletePlugin.js +2 -0
  18. package/dist/es2019/entry-points/autocompletePluginType.js +0 -0
  19. package/dist/es2019/entry-points/src-pm-plugins-autocomplete-plugin.js +2 -0
  20. package/dist/es2019/entry-points/src-pm-plugins-slow-lane-client.js +2 -0
  21. package/dist/es2019/entry-points/src-pm-plugins-text-predictor.js +2 -0
  22. package/dist/es2019/pm-plugins/autocomplete-plugin.js +132 -72
  23. package/dist/es2019/pm-plugins/local-slow-lane-client.js +355 -0
  24. package/dist/es2019/pm-plugins/slow-lane-client.js +29 -2
  25. package/dist/es2019/pm-plugins/text-predictor.js +48 -15
  26. package/dist/esm/analytics/ufo.js +105 -0
  27. package/dist/esm/entry-points/autocompletePlugin.js +2 -0
  28. package/dist/esm/entry-points/autocompletePluginType.js +0 -0
  29. package/dist/esm/entry-points/src-pm-plugins-autocomplete-plugin.js +2 -0
  30. package/dist/esm/entry-points/src-pm-plugins-slow-lane-client.js +2 -0
  31. package/dist/esm/entry-points/src-pm-plugins-text-predictor.js +2 -0
  32. package/dist/esm/pm-plugins/autocomplete-plugin.js +126 -64
  33. package/dist/esm/pm-plugins/local-slow-lane-client.js +439 -0
  34. package/dist/esm/pm-plugins/slow-lane-client.js +45 -16
  35. package/dist/esm/pm-plugins/text-predictor.js +73 -40
  36. package/dist/types/analytics/ufo.d.ts +38 -0
  37. package/dist/types/entry-points/autocompletePlugin.d.ts +1 -0
  38. package/dist/types/entry-points/autocompletePluginType.d.ts +1 -0
  39. package/dist/types/entry-points/src-pm-plugins-autocomplete-plugin.d.ts +2 -0
  40. package/dist/types/entry-points/src-pm-plugins-slow-lane-client.d.ts +2 -0
  41. package/dist/types/entry-points/src-pm-plugins-text-predictor.d.ts +2 -0
  42. package/dist/types/pm-plugins/autocomplete-plugin.d.ts +5 -0
  43. package/dist/types/pm-plugins/local-slow-lane-client.d.ts +102 -0
  44. package/dist/types-ts4.5/analytics/ufo.d.ts +38 -0
  45. package/dist/types-ts4.5/entry-points/autocompletePlugin.d.ts +1 -0
  46. package/dist/types-ts4.5/entry-points/autocompletePluginType.d.ts +1 -0
  47. package/dist/types-ts4.5/entry-points/src-pm-plugins-autocomplete-plugin.d.ts +2 -0
  48. package/dist/types-ts4.5/entry-points/src-pm-plugins-slow-lane-client.d.ts +2 -0
  49. package/dist/types-ts4.5/entry-points/src-pm-plugins-text-predictor.d.ts +2 -0
  50. package/dist/types-ts4.5/pm-plugins/autocomplete-plugin.d.ts +5 -0
  51. package/dist/types-ts4.5/pm-plugins/local-slow-lane-client.d.ts +102 -0
  52. package/package.json +8 -3
  53. package/src/analytics/ufo.ts +139 -0
  54. package/src/entry-points/autocompletePlugin.tsx +2 -0
  55. package/src/entry-points/autocompletePluginType.ts +2 -0
  56. package/src/entry-points/src-pm-plugins-autocomplete-plugin.ts +7 -0
  57. package/src/entry-points/src-pm-plugins-slow-lane-client.ts +13 -0
  58. package/src/entry-points/src-pm-plugins-text-predictor.ts +14 -0
  59. package/src/pm-plugins/autocomplete-plugin/package.json +5 -5
  60. package/src/pm-plugins/autocomplete-plugin.ts +126 -64
  61. package/src/pm-plugins/local-slow-lane-client.ts +480 -0
  62. package/src/pm-plugins/slow-lane-client/package.json +5 -5
  63. package/src/pm-plugins/slow-lane-client.ts +28 -2
  64. package/src/pm-plugins/text-predictor/package.json +5 -5
  65. package/src/pm-plugins/text-predictor.ts +42 -12
  66. package/tsconfig.app.json +3 -0
@@ -0,0 +1,139 @@
1
+ /**
2
+ * UFO experience tracking helpers for `@atlaskit/editor-plugin-autocomplete`.
3
+ *
4
+ * Pattern follows `packages/linking-platform/smart-card/src/state/analytics/ufoExperiences.ts`
5
+ * (ConcurrentExperience keyed per request/instance) plus a try/catch safety wrap
6
+ * inspired by `packages/editor/collab-provider/src/analytics/ufo.ts` so a UFO
7
+ * runtime error can never break the autocomplete feature.
8
+ *
9
+ * Experience names must be registered in DataPortal's `task` attribute before
10
+ * they can power FE Reliability SLOs:
11
+ * https://hello.atlassian.net/wiki/spaces/AA6/pages/3961393753
12
+ */
13
+
14
+ import {
15
+ ConcurrentExperience,
16
+ type CustomData,
17
+ ExperiencePerformanceTypes,
18
+ ExperienceTypes,
19
+ } from '@atlaskit/ufo';
20
+
21
+ /**
22
+ * Experience name strings are surfaced downstream by the UFO pipeline as
23
+ * `platform.fe.<type>.<platform.component>.<name>` (e.g.
24
+ * `platform.fe.operation.editor-plugin-autocomplete.slow-lane-fetch`), so
25
+ * names here should be short, dot-free, and must not repeat the component
26
+ * name. This matches the convention used by `@atlaskit/emoji`'s
27
+ * `ufoExperiences` map (e.g. `'emoji-rendered'`).
28
+ */
29
+ /**
30
+ * Only experiences with meaningful async work and a real success/failure
31
+ * outcome belong on UFO (which measures latency and success rate for FE
32
+ * Reliability SLOs). Per-keystroke suggestion lifecycle counters (viewed /
33
+ * inserted / dismissed) are tracked via analytics-next instead — they would
34
+ * otherwise emit zero-duration events on every word boundary.
35
+ */
36
+ export const EXPERIENCE_NAME = {
37
+ SLOW_LANE_FETCH: 'slow-lane-fetch',
38
+ LOAD_VOCABULARY: 'load-vocabulary',
39
+ LOAD_VECTORS: 'load-vectors',
40
+ } as const;
41
+
42
+ export type AutocompleteExperienceName =
43
+ (typeof EXPERIENCE_NAME)[keyof typeof EXPERIENCE_NAME];
44
+
45
+ const isUfoEnabled = (): boolean => !process?.env?.REACT_SSR;
46
+
47
+ const operationConfig = {
48
+ platform: { component: 'editor-plugin-autocomplete' },
49
+ type: ExperienceTypes.Operation,
50
+ performanceType: ExperiencePerformanceTypes.Custom,
51
+ performanceConfig: {
52
+ histogram: {
53
+ [ExperiencePerformanceTypes.Custom]: {
54
+ duration: '50_100_250_500_1000_2000_4000',
55
+ },
56
+ },
57
+ },
58
+ };
59
+
60
+ const experiences: Record<AutocompleteExperienceName, ConcurrentExperience> = {
61
+ [EXPERIENCE_NAME.SLOW_LANE_FETCH]: new ConcurrentExperience(
62
+ EXPERIENCE_NAME.SLOW_LANE_FETCH,
63
+ operationConfig,
64
+ ),
65
+ [EXPERIENCE_NAME.LOAD_VOCABULARY]: new ConcurrentExperience(
66
+ EXPERIENCE_NAME.LOAD_VOCABULARY,
67
+ operationConfig,
68
+ ),
69
+ [EXPERIENCE_NAME.LOAD_VECTORS]: new ConcurrentExperience(
70
+ EXPERIENCE_NAME.LOAD_VECTORS,
71
+ operationConfig,
72
+ ),
73
+ };
74
+
75
+ export const startExp = (
76
+ name: AutocompleteExperienceName,
77
+ id: string,
78
+ metadata?: CustomData,
79
+ ): void => {
80
+ if (!isUfoEnabled()) {
81
+ return;
82
+ }
83
+ try {
84
+ const instance = experiences[name].getInstance(id);
85
+ instance.start();
86
+ if (metadata) {
87
+ instance.addMetadata(metadata);
88
+ }
89
+ } catch {
90
+ // UFO errors must never break the plugin
91
+ }
92
+ };
93
+
94
+ export const succeedExp = (
95
+ name: AutocompleteExperienceName,
96
+ id: string,
97
+ metadata?: CustomData,
98
+ ): void => {
99
+ if (!isUfoEnabled()) {
100
+ return;
101
+ }
102
+ try {
103
+ experiences[name].getInstance(id).success({ metadata });
104
+ } catch {
105
+ // UFO errors must never break the plugin
106
+ }
107
+ };
108
+
109
+ export const failExp = (
110
+ name: AutocompleteExperienceName,
111
+ id: string,
112
+ metadata?: CustomData,
113
+ ): void => {
114
+ if (!isUfoEnabled()) {
115
+ return;
116
+ }
117
+ try {
118
+ experiences[name].getInstance(id).failure({ metadata });
119
+ } catch {
120
+ // UFO errors must never break the plugin
121
+ }
122
+ };
123
+
124
+ export const abortExp = (
125
+ name: AutocompleteExperienceName,
126
+ id: string,
127
+ reason?: string,
128
+ ): void => {
129
+ if (!isUfoEnabled()) {
130
+ return;
131
+ }
132
+ try {
133
+ experiences[name]
134
+ .getInstance(id)
135
+ .abort(reason ? { metadata: { reason } } : undefined);
136
+ } catch {
137
+ // UFO errors must never break the plugin
138
+ }
139
+ };
@@ -0,0 +1,2 @@
1
+ /* eslint-disable @atlaskit/editor/no-re-export */
2
+ export { autocompletePlugin } from '../autocompletePlugin';
@@ -0,0 +1,2 @@
1
+ /* eslint-disable @atlaskit/editor/no-re-export */
2
+ export type { AutocompletePlugin } from '../autocompletePluginType';
@@ -0,0 +1,7 @@
1
+ /* eslint-disable @atlaskit/editor/no-re-export */
2
+ export { autocompletePluginKey, createAutocompletePlugin } from '../pm-plugins/autocomplete-plugin';
3
+ export type {
4
+ AutocompleteContext,
5
+ AutocompletePluginOptions,
6
+ AutocompletePluginState,
7
+ } from '../pm-plugins/autocomplete-plugin';
@@ -0,0 +1,13 @@
1
+ /* eslint-disable @atlaskit/editor/no-re-export */
2
+ export {
3
+ createSlowLaneClient,
4
+ getStoredContextVector,
5
+ getStoredLmLogits,
6
+ isWordBoundary,
7
+ setDefaultSlowLaneClient,
8
+ } from '../pm-plugins/slow-lane-client';
9
+ export type {
10
+ SlowLaneClientConfig,
11
+ TypeaheadEncodingsRequest,
12
+ TypeaheadEncodingsResponse,
13
+ } from '../pm-plugins/slow-lane-client';
@@ -0,0 +1,14 @@
1
+ /* eslint-disable @atlaskit/editor/no-re-export */
2
+ export {
3
+ getLastPredictionDebug,
4
+ getPredictorStatus,
5
+ incrementSessionFreq,
6
+ ingestDocumentPage,
7
+ initL3Vocabulary,
8
+ initVectors,
9
+ initVocabulary,
10
+ loadDefaultVocabulary,
11
+ loadVectorsAsync,
12
+ predict,
13
+ } from '../pm-plugins/text-predictor';
14
+ export type { TenantVocabulary, WeightedTerm } from '../pm-plugins/text-predictor';
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-autocomplete/src/pm-plugins/autocomplete-plugin",
3
- "main": "../../../dist/cjs/pm-plugins/autocomplete-plugin.js",
4
- "module": "../../../dist/esm/pm-plugins/autocomplete-plugin.js",
5
- "module:es2019": "../../../dist/es2019/pm-plugins/autocomplete-plugin.js",
3
+ "main": "../../../dist/cjs/entry-points/src-pm-plugins-autocomplete-plugin.js",
4
+ "module": "../../../dist/esm/entry-points/src-pm-plugins-autocomplete-plugin.js",
5
+ "module:es2019": "../../../dist/es2019/entry-points/src-pm-plugins-autocomplete-plugin.js",
6
6
  "sideEffects": false,
7
- "types": "../../../dist/types/pm-plugins/autocomplete-plugin.d.ts",
7
+ "types": "../../../dist/types/entry-points/src-pm-plugins-autocomplete-plugin.d.ts",
8
8
  "typesVersions": {
9
9
  ">=4.5 <5.9": {
10
10
  "*": [
11
- "../../../dist/types-ts4.5/pm-plugins/autocomplete-plugin.d.ts"
11
+ "../../../dist/types-ts4.5/entry-points/src-pm-plugins-autocomplete-plugin.d.ts"
12
12
  ]
13
13
  }
14
14
  }
@@ -1,4 +1,5 @@
1
1
  import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
2
+ import { logException } from '@atlaskit/editor-common/monitoring';
2
3
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
3
4
  import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
4
5
  import { keydownHandler } from '@atlaskit/editor-prosemirror/keymap';
@@ -14,6 +15,7 @@ import type { EditorView } from '@atlaskit/editor-prosemirror/view';
14
15
  import type { AutocompletePlugin } from '../autocompletePluginType';
15
16
 
16
17
  import { createGhostTextDecorationSet } from './ghost-text-decoration';
18
+ import { createLocalSlowLaneClient, type LocalSlowLaneClient } from './local-slow-lane-client';
17
19
  import { createSlowLaneClient, setDefaultSlowLaneClient, isWordBoundary } from './slow-lane-client';
18
20
  import {
19
21
  predict,
@@ -23,12 +25,15 @@ import {
23
25
  ingestDocumentPage,
24
26
  } from './text-predictor';
25
27
 
26
- const SLOW_LANE_ENDPOINT = '/gateway/api/v1/autocomplete/typeahead-encodings';
27
-
28
28
  export const autocompletePluginKey: PluginKey = new PluginKey('autocomplete');
29
29
 
30
30
  const DEBOUNCE_MS = 150;
31
31
 
32
+ const hasDestroy = (
33
+ client: ReturnType<typeof createSlowLaneClient> | LocalSlowLaneClient,
34
+ ): client is LocalSlowLaneClient =>
35
+ 'destroy' in client && typeof client.destroy === 'function';
36
+
32
37
  export interface AutocompletePluginState {
33
38
  /** The decoration set containing the ghost text widget */
34
39
  decorationSet: DecorationSet;
@@ -97,14 +102,18 @@ const setAutocompleteMeta = (
97
102
  let lastShownGhostText = '';
98
103
 
99
104
  const showGhostText = (view: EditorView, text: string, position: number): void => {
100
- const { state, dispatch } = view;
101
- const decorationSet = createGhostTextDecorationSet(state, position, text);
102
- const tr = setAutocompleteMeta(state.tr, {
103
- ghostText: text,
104
- ghostPosition: position,
105
- decorationSet,
106
- });
107
- dispatch(tr);
105
+ try {
106
+ const { state, dispatch } = view;
107
+ const decorationSet = createGhostTextDecorationSet(state, position, text);
108
+ const tr = setAutocompleteMeta(state.tr, {
109
+ ghostText: text,
110
+ ghostPosition: position,
111
+ decorationSet,
112
+ });
113
+ dispatch(tr);
114
+ } catch (error) {
115
+ logException(error as Error, { location: 'editor-plugin-autocomplete/showGhostText' });
116
+ }
108
117
  };
109
118
 
110
119
  /**
@@ -117,12 +126,17 @@ const clearGhostText = (state: EditorState, dispatch?: (tr: Transaction) => void
117
126
  }
118
127
 
119
128
  if (dispatch) {
120
- const tr = setAutocompleteMeta(state.tr, {
121
- ghostText: '',
122
- ghostPosition: -1,
123
- decorationSet: DecorationSet.empty,
124
- });
125
- dispatch(tr);
129
+ try {
130
+ const tr = setAutocompleteMeta(state.tr, {
131
+ ghostText: '',
132
+ ghostPosition: -1,
133
+ decorationSet: DecorationSet.empty,
134
+ });
135
+ dispatch(tr);
136
+ } catch (error) {
137
+ logException(error as Error, { location: 'editor-plugin-autocomplete/clearGhostText' });
138
+ return false;
139
+ }
126
140
  }
127
141
  return true;
128
142
  };
@@ -137,14 +151,19 @@ const acceptGhostText = (state: EditorState, dispatch?: (tr: Transaction) => voi
137
151
  }
138
152
 
139
153
  if (dispatch) {
140
- const { ghostText, ghostPosition } = pluginState;
141
- let tr = state.tr.insertText(ghostText, ghostPosition);
142
- tr = setAutocompleteMeta(tr, {
143
- ghostText: '',
144
- ghostPosition: -1,
145
- decorationSet: DecorationSet.empty,
146
- });
147
- dispatch(tr);
154
+ try {
155
+ const { ghostText, ghostPosition } = pluginState;
156
+ let tr = state.tr.insertText(ghostText, ghostPosition);
157
+ tr = setAutocompleteMeta(tr, {
158
+ ghostText: '',
159
+ ghostPosition: -1,
160
+ decorationSet: DecorationSet.empty,
161
+ });
162
+ dispatch(tr);
163
+ } catch (error) {
164
+ logException(error as Error, { location: 'editor-plugin-autocomplete/acceptGhostText' });
165
+ return false;
166
+ }
148
167
  }
149
168
  return true;
150
169
  };
@@ -178,6 +197,11 @@ export interface AutocompletePluginOptions {
178
197
  * Use this to serve vectors from a CDN or media service in production.
179
198
  */
180
199
  getVectorsBinaryUrl?: () => Promise<string>;
200
+ /**
201
+ * When true, uses on-device inference via WebGPU (MLC WebLLM) instead of
202
+ * the network-based slow-lane backend. Defaults to false (network client).
203
+ */
204
+ useLocalModel?: boolean;
181
205
  }
182
206
 
183
207
  /**
@@ -235,6 +259,15 @@ export const createAutocompletePlugin = (
235
259
  let lastSuggestionTypedLength = 0;
236
260
  let lastSuggestionLength = 0;
237
261
 
262
+ const fireSuggestionDismissedAnalytics = (reason: 'escape' | 'blur'): void => {
263
+ api?.analytics?.actions.fireAnalyticsEvent({
264
+ action: ACTION.SUGGESTION_DISMISSED,
265
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
266
+ eventType: EVENT_TYPE.TRACK,
267
+ attributes: { reason },
268
+ });
269
+ };
270
+
238
271
  const fireSuggestionInsertedAnalytics = (ghostText: string): void => {
239
272
  const typedLength = lastSuggestionTypedLength;
240
273
  const suggestionLength = lastSuggestionLength || typedLength + ghostText.length;
@@ -252,10 +285,14 @@ export const createAutocompletePlugin = (
252
285
  });
253
286
  };
254
287
 
255
- const slowLaneClient = createSlowLaneClient({
256
- baseUrl: '',
257
- endpoint: SLOW_LANE_ENDPOINT,
258
- });
288
+ const slowLaneClient = options?.useLocalModel
289
+ ? createLocalSlowLaneClient({
290
+ debounceMs: 300,
291
+ })
292
+ : createSlowLaneClient({
293
+ baseUrl: '',
294
+ debounceMs: 300,
295
+ });
259
296
  setDefaultSlowLaneClient(slowLaneClient);
260
297
 
261
298
  /**
@@ -269,45 +306,51 @@ export const createAutocompletePlugin = (
269
306
  }
270
307
 
271
308
  debounceTimer = setTimeout(() => {
272
- const { state } = view;
273
- const { selection } = state;
309
+ try {
310
+ const { state } = view;
311
+ const { selection } = state;
274
312
 
275
- // Only predict for cursor selections (not range selections)
276
- if (!selection.empty) {
277
- return;
278
- }
313
+ // Only predict for cursor selections (not range selections)
314
+ if (!selection.empty) {
315
+ return;
316
+ }
279
317
 
280
- const textBefore = getTextBeforeCursor(state);
318
+ const textBefore = getTextBeforeCursor(state);
281
319
 
282
- // Suppress re-showing the same suggestion the user just dismissed.
283
- // Once the text context changes (user types or deletes), this clears automatically.
284
- if (textBefore === dismissedContext) {
285
- return;
286
- }
287
- dismissedContext = null;
320
+ // Suppress re-showing the same suggestion the user just dismissed.
321
+ // Once the text context changes (user types or deletes), this clears automatically.
322
+ if (textBefore === dismissedContext) {
323
+ return;
324
+ }
325
+ dismissedContext = null;
288
326
 
289
- // Don't predict if there's not enough context
290
- if (textBefore.trim().length < 3) {
291
- return;
292
- }
327
+ // Don't predict if there's not enough context
328
+ if (textBefore.trim().length < 3) {
329
+ return;
330
+ }
293
331
 
294
- // Tier 1 prediction is synchronous -- no async needed
295
- const prediction = predict(textBefore);
296
-
297
- if (prediction && prediction.length > 0) {
298
- const typedLength = getTypedLengthForPrediction(textBefore);
299
- lastSuggestionTypedLength = typedLength;
300
- lastSuggestionLength = typedLength + prediction.length;
301
-
302
- showGhostText(view, prediction, selection.from);
303
- if (prediction !== lastShownGhostText) {
304
- lastShownGhostText = prediction;
305
- api?.analytics?.actions.fireAnalyticsEvent({
306
- action: ACTION.SUGGESTION_VIEWED,
307
- actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
308
- eventType: EVENT_TYPE.TRACK,
309
- });
332
+ // Tier 1 prediction is synchronous -- no async needed
333
+ const prediction = predict(textBefore);
334
+
335
+ if (prediction && prediction.length > 0) {
336
+ const typedLength = getTypedLengthForPrediction(textBefore);
337
+ lastSuggestionTypedLength = typedLength;
338
+ lastSuggestionLength = typedLength + prediction.length;
339
+
340
+ showGhostText(view, prediction, selection.from);
341
+ if (prediction !== lastShownGhostText) {
342
+ lastShownGhostText = prediction;
343
+ api?.analytics?.actions.fireAnalyticsEvent({
344
+ action: ACTION.SUGGESTION_VIEWED,
345
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
346
+ eventType: EVENT_TYPE.TRACK,
347
+ });
348
+ }
310
349
  }
350
+ } catch (error) {
351
+ logException(error as Error, {
352
+ location: 'editor-plugin-autocomplete/schedulePrediction',
353
+ });
311
354
  }
312
355
  }, DEBOUNCE_MS);
313
356
  };
@@ -424,6 +467,7 @@ export const createAutocompletePlugin = (
424
467
  const didClear = clearGhostText(state, dispatch);
425
468
  if (didClear) {
426
469
  dismissedContext = getTextBeforeCursor(state);
470
+ fireSuggestionDismissedAnalytics('escape');
427
471
  }
428
472
  return didClear;
429
473
  },
@@ -436,12 +480,23 @@ export const createAutocompletePlugin = (
436
480
  | undefined;
437
481
  if (pluginState?.ghostText) {
438
482
  clearGhostText(view.state, view.dispatch);
483
+ fireSuggestionDismissedAnalytics('blur');
439
484
  }
440
485
  return false;
441
486
  },
442
487
  focus: () => {
443
- loadDefaultVocabulary();
444
- loadVectorsAsync({ getBinaryUrl: options?.getVectorsBinaryUrl }).catch(() => {});
488
+ try {
489
+ loadDefaultVocabulary();
490
+ } catch (error) {
491
+ logException(error as Error, {
492
+ location: 'editor-plugin-autocomplete/loadDefaultVocabulary',
493
+ });
494
+ }
495
+ loadVectorsAsync({ getBinaryUrl: options?.getVectorsBinaryUrl }).catch((error) => {
496
+ logException(error as Error, {
497
+ location: 'editor-plugin-autocomplete/loadVectorsAsync',
498
+ });
499
+ });
445
500
  if (!hasIngestedPage) {
446
501
  hasIngestedPage = true;
447
502
  if (options?.getContext) {
@@ -461,7 +516,11 @@ export const createAutocompletePlugin = (
461
516
  }
462
517
  context.siblingCommentsContents?.forEach(ingestDocumentPage);
463
518
  })
464
- .catch(() => {});
519
+ .catch((error) => {
520
+ logException(error as Error, {
521
+ location: 'editor-plugin-autocomplete/getContext',
522
+ });
523
+ });
465
524
  }
466
525
  }
467
526
  return false;
@@ -503,6 +562,9 @@ export const createAutocompletePlugin = (
503
562
  if (debounceTimer) {
504
563
  clearTimeout(debounceTimer);
505
564
  }
565
+ if (hasDestroy(slowLaneClient)) {
566
+ slowLaneClient.destroy();
567
+ }
506
568
  setDefaultSlowLaneClient(null);
507
569
  },
508
570
  }),