@atlaskit/editor-synced-block-provider 8.2.0 → 8.3.1

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 (40) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cjs/clients/block-service/ari.js +7 -3
  3. package/dist/cjs/clients/block-service/blockSubscription.js +4 -2
  4. package/dist/cjs/clients/confluence/ari.js +3 -1
  5. package/dist/cjs/clients/jira/ari.js +3 -1
  6. package/dist/cjs/hooks/useFetchSyncBlockData.js +1 -1
  7. package/dist/cjs/providers/block-service/blockServiceAPI.js +11 -6
  8. package/dist/cjs/store-manager/referenceSyncBlockStoreManager.js +9 -4
  9. package/dist/cjs/store-manager/syncBlockBatchFetcher.js +2 -1
  10. package/dist/cjs/store-manager/syncBlockProviderFactoryManager.js +4 -3
  11. package/dist/cjs/store-manager/syncBlockSubscriptionManager.js +11 -4
  12. package/dist/cjs/utils/errorHandling.js +152 -6
  13. package/dist/cjs/utils/utils.js +2 -1
  14. package/dist/es2019/clients/block-service/ari.js +7 -3
  15. package/dist/es2019/clients/block-service/blockSubscription.js +4 -2
  16. package/dist/es2019/clients/confluence/ari.js +3 -1
  17. package/dist/es2019/clients/jira/ari.js +3 -1
  18. package/dist/es2019/hooks/useFetchSyncBlockData.js +2 -2
  19. package/dist/es2019/providers/block-service/blockServiceAPI.js +15 -5
  20. package/dist/es2019/store-manager/referenceSyncBlockStoreManager.js +10 -5
  21. package/dist/es2019/store-manager/syncBlockBatchFetcher.js +3 -2
  22. package/dist/es2019/store-manager/syncBlockProviderFactoryManager.js +5 -4
  23. package/dist/es2019/store-manager/syncBlockSubscriptionManager.js +12 -5
  24. package/dist/es2019/utils/errorHandling.js +167 -22
  25. package/dist/es2019/utils/utils.js +2 -1
  26. package/dist/esm/clients/block-service/ari.js +7 -3
  27. package/dist/esm/clients/block-service/blockSubscription.js +4 -2
  28. package/dist/esm/clients/confluence/ari.js +3 -1
  29. package/dist/esm/clients/jira/ari.js +3 -1
  30. package/dist/esm/hooks/useFetchSyncBlockData.js +2 -2
  31. package/dist/esm/providers/block-service/blockServiceAPI.js +11 -6
  32. package/dist/esm/store-manager/referenceSyncBlockStoreManager.js +10 -5
  33. package/dist/esm/store-manager/syncBlockBatchFetcher.js +3 -2
  34. package/dist/esm/store-manager/syncBlockProviderFactoryManager.js +5 -4
  35. package/dist/esm/store-manager/syncBlockSubscriptionManager.js +12 -5
  36. package/dist/esm/utils/errorHandling.js +149 -5
  37. package/dist/esm/utils/utils.js +2 -1
  38. package/dist/types/providers/types.d.ts +8 -0
  39. package/dist/types/utils/errorHandling.d.ts +94 -2
  40. package/package.json +3 -3
@@ -1,5 +1,7 @@
1
1
  /* eslint-disable require-unicode-regexp */
2
2
 
3
+ const JIRA_WORK_ITEM_ARI_REGEX = /ari:cloud:jira:([^:]+):issue\/(\d+)/;
4
+
3
5
  /**
4
6
  * Generates the Jira work item ARI
5
7
  * @param workItemId - the ID of the work item
@@ -21,7 +23,7 @@ export const getJiraWorkItemAri = ({
21
23
  export const getJiraWorkItemIdFromAri = ({
22
24
  ari
23
25
  }) => {
24
- const match = ari.match(/ari:cloud:jira:([^:]+):issue\/(\d+)/);
26
+ const match = ari.match(JIRA_WORK_ITEM_ARI_REGEX);
25
27
  if (match !== null && match !== void 0 && match[2]) {
26
28
  return match[2];
27
29
  }
@@ -3,7 +3,7 @@ import { isSSR } from '@atlaskit/editor-common/core-utils';
3
3
  import { logException } from '@atlaskit/editor-common/monitoring';
4
4
  import { fg } from '@atlaskit/platform-feature-flags';
5
5
  import { isProviderNotReadyError, SyncBlockError } from '../common/types';
6
- import { fetchErrorPayload } from '../utils/errorHandling';
6
+ import { buildFetchErrorAttribution, fetchErrorPayload } from '../utils/errorHandling';
7
7
  import { createSyncBlockNode, getSourceProductFromResourceIdSafe } from '../utils/utils';
8
8
  export const useFetchSyncBlockData = (manager, resourceId, localId, fireAnalyticsEvent) => {
9
9
  var _manager$referenceMan2, _manager$referenceMan3, _manager$referenceMan4;
@@ -72,7 +72,7 @@ export const useFetchSyncBlockData = (manager, resourceId, localId, fireAnalytic
72
72
  logException(error, {
73
73
  location: 'editor-synced-block-provider/useFetchSyncBlockData'
74
74
  });
75
- fireAnalyticsEvent === null || fireAnalyticsEvent === void 0 ? void 0 : fireAnalyticsEvent(fetchErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
75
+ fireAnalyticsEvent === null || fireAnalyticsEvent === void 0 ? void 0 : fireAnalyticsEvent(fetchErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId), buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), error.message)));
76
76
 
77
77
  // Set error state if fetching fails
78
78
  setFetchState({
@@ -7,6 +7,8 @@ import { SyncBlockError } from '../../common/types';
7
7
  import { stringifyError } from '../../utils/errorHandling';
8
8
  import { createResourceIdForReference } from '../../utils/resourceId';
9
9
  import { convertContentUpdatedAt } from '../../utils/utils';
10
+ const BLOCK_ARI_TO_RESOURCE_ID_REGEX = /^ari:cloud:blocks:.*:synced-block\/(.+)$/;
11
+ const EXTRACT_RESOURCE_ID_FROM_BLOCK_ARI_REGEX = /ari:cloud:blocks:[^:]+:synced-block\/(.+)$/;
10
12
  const mapBlockError = error => {
11
13
  switch (error.status) {
12
14
  case 400:
@@ -73,7 +75,7 @@ export const blockAriToResourceId = blockAri => {
73
75
  // The regex captures the full path after synced-block/
74
76
  // e.g. ari:cloud:blocks:DUMMY-a5a01d21-1cc3-4f29-9565-f2bb8cd969f5:synced-block/confluence-page/455232061495/e8cf64e3-1b6e-489b-ad86-8465b0905bb4
75
77
  // should return confluence-page/455232061495/e8cf64e3-1b6e-489b-ad86-8465b0905bb4
76
- const match = blockAri.match(/^ari:cloud:blocks:.*:synced-block\/(.+)$/);
78
+ const match = blockAri.match(BLOCK_ARI_TO_RESOURCE_ID_REGEX);
77
79
  return (match === null || match === void 0 ? void 0 : match[1]) || null;
78
80
  };
79
81
 
@@ -160,7 +162,7 @@ export const fetchReferences = async documentAri => {
160
162
  * Block ARI format: ari:cloud:blocks:<cloudId>:synced-block/<resourceId>
161
163
  */
162
164
  export const extractResourceIdFromBlockAri = blockAri => {
163
- const match = blockAri.match(/ari:cloud:blocks:[^:]+:synced-block\/(.+)$/);
165
+ const match = blockAri.match(EXTRACT_RESOURCE_ID_FROM_BLOCK_ARI_REGEX);
164
166
  return match === null || match === void 0 ? void 0 : match[1];
165
167
  };
166
168
 
@@ -300,11 +302,16 @@ export const batchFetchData = async (cloudId, parentAri, blockNodeIdentifiers, c
300
302
  }));
301
303
  }
302
304
 
303
- // If batch request fails, return error for all resourceIds
305
+ // If batch request fails, return error for all resourceIds. Capture the HTTP
306
+ // status from `BlockError` so fetch analytics can break failures down by
307
+ // statusCode (EDITOR-7862); undefined for non-HTTP failures.
304
308
  return blockNodeIdentifiers.map(blockNodeIdentifier => ({
305
309
  error: {
306
310
  type: error instanceof BlockError ? mapBlockError(error) : SyncBlockError.Errored,
307
- reason: error.message
311
+ reason: error.message,
312
+ ...(error instanceof BlockError && {
313
+ statusCode: error.status
314
+ })
308
315
  },
309
316
  resourceId: blockNodeIdentifier.resourceId
310
317
  }));
@@ -458,10 +465,13 @@ class BlockServiceADFFetchProvider {
458
465
  };
459
466
  } catch (error) {
460
467
  if (error instanceof BlockError) {
468
+ // Capture the HTTP status so fetch analytics can break failures down by
469
+ // statusCode (EDITOR-7862).
461
470
  return {
462
471
  error: {
463
472
  type: mapBlockError(error),
464
- reason: error.message
473
+ reason: error.message,
474
+ statusCode: error.status
465
475
  },
466
476
  resourceId
467
477
  };
@@ -3,7 +3,7 @@ import isEqual from 'lodash/isEqual';
3
3
  import { logException } from '@atlaskit/editor-common/monitoring';
4
4
  import { fg } from '@atlaskit/platform-feature-flags';
5
5
  import { isProviderNotReadyError, ProviderNotReadyError, SyncBlockError } from '../common/types';
6
- import { buildErrorAttribution, cacheDeletionForcedPayload, fetchErrorPayload, fetchSuccessPayload, getSourceInfoErrorPayload, sourceInfoOrphanedPayload, updateReferenceErrorPayload } from '../utils/errorHandling';
6
+ import { buildErrorAttribution, buildFetchErrorAttribution, cacheDeletionForcedPayload, fetchErrorPayload, fetchSuccessPayload, getSourceInfoErrorPayload, sourceInfoOrphanedPayload, updateReferenceErrorPayload } from '../utils/errorHandling';
7
7
  import { getFetchExperience, getFetchSourceInfoExperience, getSaveReferenceExperience } from '../utils/experienceTracking';
8
8
  import { resolveSyncBlockInstance } from '../utils/resolveSyncBlockInstance';
9
9
  import { createSyncBlockNode, getSourceProductFromResourceIdSafe, normalizeSyncBlockJSONContent } from '../utils/utils';
@@ -472,10 +472,12 @@ export class ReferenceSyncBlockStoreManager {
472
472
  data.forEach(syncBlockInstance => {
473
473
  var _resolvedSyncBlockIns;
474
474
  if (!syncBlockInstance.resourceId) {
475
- var _syncBlockInstance$er, _syncBlockInstance$er2, _this$fireAnalyticsEv8;
475
+ var _syncBlockInstance$er, _syncBlockInstance$er2, _this$fireAnalyticsEv8, _syncBlockInstance$er3, _syncBlockInstance$er4, _syncBlockInstance$er5;
476
476
  const payload = ((_syncBlockInstance$er = syncBlockInstance.error) === null || _syncBlockInstance$er === void 0 ? void 0 : _syncBlockInstance$er.reason) || ((_syncBlockInstance$er2 = syncBlockInstance.error) === null || _syncBlockInstance$er2 === void 0 ? void 0 : _syncBlockInstance$er2.type) || 'Returned sync block instance does not have resource id';
477
477
  // No resourceId means we cannot derive a sourceProduct here; intentionally omit.
478
- (_this$fireAnalyticsEv8 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv8 === void 0 ? void 0 : _this$fireAnalyticsEv8.call(this, fetchErrorPayload(payload));
478
+ // Classify on the structured `type` first, falling back to the free-text
479
+ // `reason`/payload (EDITOR-7862).
480
+ (_this$fireAnalyticsEv8 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv8 === void 0 ? void 0 : _this$fireAnalyticsEv8.call(this, fetchErrorPayload(payload, undefined, undefined, buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), ((_syncBlockInstance$er3 = syncBlockInstance.error) === null || _syncBlockInstance$er3 === void 0 ? void 0 : _syncBlockInstance$er3.type) || ((_syncBlockInstance$er4 = syncBlockInstance.error) === null || _syncBlockInstance$er4 === void 0 ? void 0 : _syncBlockInstance$er4.reason) || payload, (_syncBlockInstance$er5 = syncBlockInstance.error) === null || _syncBlockInstance$er5 === void 0 ? void 0 : _syncBlockInstance$er5.statusCode)));
479
481
  return;
480
482
  }
481
483
  const existingSyncBlock = this.getFromCache(syncBlockInstance.resourceId);
@@ -509,7 +511,10 @@ export class ReferenceSyncBlockStoreManager {
509
511
  const isRetryingEntityNotFound = syncBlockInstance.error.type === SyncBlockError.EntityNotFound && ((_this$entityNotFoundR = this.entityNotFoundRetryCount.get(syncBlockInstance.resourceId)) !== null && _this$entityNotFoundR !== void 0 ? _this$entityNotFoundR : 0) < ENTITY_NOT_FOUND_MAX_RETRIES && fg('platform_synced_block_patch_13');
510
512
  if (!isRetryingEntityNotFound) {
511
513
  var _this$fireAnalyticsEv9, _syncBlockInstance$da, _syncBlockInstance$da2;
512
- (_this$fireAnalyticsEv9 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv9 === void 0 ? void 0 : _this$fireAnalyticsEv9.call(this, fetchErrorPayload(syncBlockInstance.error.reason || syncBlockInstance.error.type, syncBlockInstance.resourceId, (_syncBlockInstance$da = (_syncBlockInstance$da2 = syncBlockInstance.data) === null || _syncBlockInstance$da2 === void 0 ? void 0 : _syncBlockInstance$da2.product) !== null && _syncBlockInstance$da !== void 0 ? _syncBlockInstance$da : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId)));
514
+ // Classify on the structured `type` (a `SyncBlockError` enum value) first,
515
+ // falling back to the free-text `reason` so source-state/permission strings
516
+ // are still bucketed (EDITOR-7862). The emitted `error` attribute is unchanged.
517
+ (_this$fireAnalyticsEv9 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv9 === void 0 ? void 0 : _this$fireAnalyticsEv9.call(this, fetchErrorPayload(syncBlockInstance.error.reason || syncBlockInstance.error.type, syncBlockInstance.resourceId, (_syncBlockInstance$da = (_syncBlockInstance$da2 = syncBlockInstance.data) === null || _syncBlockInstance$da2 === void 0 ? void 0 : _syncBlockInstance$da2.product) !== null && _syncBlockInstance$da !== void 0 ? _syncBlockInstance$da : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId), buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), syncBlockInstance.error.type || syncBlockInstance.error.reason, syncBlockInstance.error.statusCode)));
513
518
  }
514
519
  if (syncBlockInstance.error.type === SyncBlockError.NotFound || syncBlockInstance.error.type === SyncBlockError.Forbidden) {
515
520
  hasExpectedError = true;
@@ -806,7 +811,7 @@ export class ReferenceSyncBlockStoreManager {
806
811
  logException(error, {
807
812
  location: 'editor-synced-block-provider/referenceSyncBlockStoreManager'
808
813
  });
809
- (_this$fireAnalyticsEv11 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv11 === void 0 ? void 0 : _this$fireAnalyticsEv11.call(this, fetchErrorPayload(error.message));
814
+ (_this$fireAnalyticsEv11 = this.fireAnalyticsEvent) === null || _this$fireAnalyticsEv11 === void 0 ? void 0 : _this$fireAnalyticsEv11.call(this, fetchErrorPayload(error.message, undefined, undefined, buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), error.message)));
810
815
  return () => {};
811
816
  }
812
817
  }
@@ -3,7 +3,7 @@ import rafSchedule from 'raf-schd';
3
3
  import { logException } from '@atlaskit/editor-common/monitoring';
4
4
  import { fg } from '@atlaskit/platform-feature-flags';
5
5
  import { isProviderNotReadyError } from '../common/types';
6
- import { fetchErrorPayload } from '../utils/errorHandling';
6
+ import { buildFetchErrorAttribution, fetchErrorPayload } from '../utils/errorHandling';
7
7
  import { createSyncBlockNode, getSourceProductFromResourceIdSafe } from '../utils/utils';
8
8
  /**
9
9
  * Handles debounced batch-fetching of sync block data via `raf-schd`.
@@ -60,9 +60,10 @@ export class SyncBlockBatchFetcher {
60
60
  logException(error, {
61
61
  location: 'editor-synced-block-provider/syncBlockBatchFetcher/batchedFetchSyncBlocks'
62
62
  });
63
+ const attribution = buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), error.message);
63
64
  resourceIds.forEach(resId => {
64
65
  var _this$deps$getFireAna;
65
- (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 ? void 0 : _this$deps$getFireAna(fetchErrorPayload(error.message, resId, getSourceProductFromResourceIdSafe(resId)));
66
+ (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 ? void 0 : _this$deps$getFireAna(fetchErrorPayload(error.message, resId, getSourceProductFromResourceIdSafe(resId), attribution));
66
67
  });
67
68
  }).finally(() => {
68
69
  // If the fetcher was destroyed while the request was in flight,
@@ -1,7 +1,8 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import { logException } from '@atlaskit/editor-common/monitoring';
3
3
  import { ProviderFactory } from '@atlaskit/editor-common/provider-factory';
4
- import { fetchErrorPayload } from '../utils/errorHandling';
4
+ import { fg } from '@atlaskit/platform-feature-flags';
5
+ import { buildFetchErrorAttribution, fetchErrorPayload } from '../utils/errorHandling';
5
6
  import { parseResourceId } from '../utils/resourceId';
6
7
  import { getSourceProductFromResourceIdSafe } from '../utils/utils';
7
8
  /**
@@ -21,7 +22,7 @@ export class SyncBlockProviderFactoryManager {
21
22
  logException(error, {
22
23
  location: 'editor-synced-block-provider/syncBlockProviderFactoryManager'
23
24
  });
24
- (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 ? void 0 : _this$deps$getFireAna(fetchErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
25
+ (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 ? void 0 : _this$deps$getFireAna(fetchErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId), buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), error.message)));
25
26
  return undefined;
26
27
  }
27
28
  const {
@@ -55,7 +56,7 @@ export class SyncBlockProviderFactoryManager {
55
56
  logException(error, {
56
57
  location: 'editor-synced-block-provider/syncBlockProviderFactoryManager'
57
58
  });
58
- (_this$deps$getFireAna2 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna2 === void 0 ? void 0 : _this$deps$getFireAna2(fetchErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
59
+ (_this$deps$getFireAna2 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna2 === void 0 ? void 0 : _this$deps$getFireAna2(fetchErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId), buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), error.message)));
59
60
  }
60
61
  }
61
62
  return providerFactory;
@@ -133,7 +134,7 @@ export class SyncBlockProviderFactoryManager {
133
134
  if (!syncBlock.data.sourceAri || !syncBlock.data.product) {
134
135
  var _this$deps$getFireAna3, _syncBlock$data$produ;
135
136
  (_this$deps$getFireAna3 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna3 === void 0 ? void 0 : _this$deps$getFireAna3(fetchErrorPayload('Sync block source ari or product not found', resourceId, // Prefer cached product when available; fall back to parsing resourceId.
136
- (_syncBlock$data$produ = syncBlock.data.product) !== null && _syncBlock$data$produ !== void 0 ? _syncBlock$data$produ : getSourceProductFromResourceIdSafe(resourceId)));
137
+ (_syncBlock$data$produ = syncBlock.data.product) !== null && _syncBlock$data$produ !== void 0 ? _syncBlock$data$produ : getSourceProductFromResourceIdSafe(resourceId), buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), 'Sync block source ari or product not found')));
137
138
  return;
138
139
  }
139
140
  const parentInfo = dataProvider.retrieveSyncBlockParentInfo(syncBlock.data.sourceAri, syncBlock.data.product);
@@ -1,7 +1,7 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import { logException } from '@atlaskit/editor-common/monitoring';
3
3
  import { fg } from '@atlaskit/platform-feature-flags';
4
- import { fetchErrorPayload, fetchSuccessPayload } from '../utils/errorHandling';
4
+ import { buildFetchErrorAttribution, fetchErrorPayload, fetchSuccessPayload } from '../utils/errorHandling';
5
5
  import { resolveSyncBlockInstance } from '../utils/resolveSyncBlockInstance';
6
6
  import { getSourceProductFromResourceIdSafe } from '../utils/utils';
7
7
  /**
@@ -87,7 +87,7 @@ export class SyncBlockSubscriptionManager {
87
87
  logException(error, {
88
88
  location: 'editor-synced-block-provider/syncBlockSubscriptionManager/notifySubscriptionChangeListeners'
89
89
  });
90
- (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 ? void 0 : _this$deps$getFireAna(fetchErrorPayload(error.message));
90
+ (_this$deps$getFireAna = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna === void 0 ? void 0 : _this$deps$getFireAna(fetchErrorPayload(error.message, undefined, undefined, buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), error.message)));
91
91
  }
92
92
  });
93
93
  }
@@ -264,6 +264,9 @@ export class SyncBlockSubscriptionManager {
264
264
  // recovers on reconnect, so under the gate we don't fire a
265
265
  // user-facing error here — it's only surfaced on exhaustion (see
266
266
  // scheduleReconnection). Gate OFF keeps the legacy fire-on-drop.
267
+ // This branch only runs when the gate is OFF, so buildFetchErrorAttribution
268
+ // would return undefined; the structured attribution (EDITOR-7862) is therefore
269
+ // applied at the gate-ON exhaustion site in scheduleReconnection instead.
267
270
  if (!fg('platform_editor_blocks_patch_3')) {
268
271
  var _this$deps$getFireAna2;
269
272
  (_this$deps$getFireAna2 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna2 === void 0 ? void 0 : _this$deps$getFireAna2(fetchErrorPayload(error.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
@@ -310,7 +313,7 @@ export class SyncBlockSubscriptionManager {
310
313
  logException(new Error(errorMessage), {
311
314
  location: 'editor-synced-block-provider/syncBlockSubscriptionManager/max-retries-exhausted'
312
315
  });
313
- (_this$deps$getFireAna3 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna3 === void 0 ? void 0 : _this$deps$getFireAna3(fetchErrorPayload(errorMessage, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
316
+ (_this$deps$getFireAna3 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna3 === void 0 ? void 0 : _this$deps$getFireAna3(fetchErrorPayload(errorMessage, resourceId, getSourceProductFromResourceIdSafe(resourceId), buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), errorMessage)));
314
317
  return;
315
318
  }
316
319
  const delay = this.getReconnectionDelay(attempts);
@@ -409,9 +412,13 @@ export class SyncBlockSubscriptionManager {
409
412
  });
410
413
  this.deps.fetchSyncBlockSourceInfo(resolved.resourceId);
411
414
  } else {
412
- var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna5, _syncBlockInstance$da3, _syncBlockInstance$da4;
415
+ var _syncBlockInstance$er, _syncBlockInstance$er2, _this$deps$getFireAna5, _syncBlockInstance$da3, _syncBlockInstance$da4, _syncBlockInstance$er3, _syncBlockInstance$er4, _syncBlockInstance$er5;
413
416
  const errorMessage = ((_syncBlockInstance$er = syncBlockInstance.error) === null || _syncBlockInstance$er === void 0 ? void 0 : _syncBlockInstance$er.reason) || ((_syncBlockInstance$er2 = syncBlockInstance.error) === null || _syncBlockInstance$er2 === void 0 ? void 0 : _syncBlockInstance$er2.type);
414
- (_this$deps$getFireAna5 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna5 === void 0 ? void 0 : _this$deps$getFireAna5(fetchErrorPayload(errorMessage, syncBlockInstance.resourceId, (_syncBlockInstance$da3 = (_syncBlockInstance$da4 = syncBlockInstance.data) === null || _syncBlockInstance$da4 === void 0 ? void 0 : _syncBlockInstance$da4.product) !== null && _syncBlockInstance$da3 !== void 0 ? _syncBlockInstance$da3 : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId)));
417
+
418
+ // Prefer the structured `type` (a `SyncBlockError` enum value) for classification
419
+ // and fall back to the free-text `reason` so source-state/permission strings are
420
+ // still bucketed (EDITOR-7862). The emitted free-text `error` attribute is unchanged.
421
+ (_this$deps$getFireAna5 = this.deps.getFireAnalyticsEvent()) === null || _this$deps$getFireAna5 === void 0 ? void 0 : _this$deps$getFireAna5(fetchErrorPayload(errorMessage, syncBlockInstance.resourceId, (_syncBlockInstance$da3 = (_syncBlockInstance$da4 = syncBlockInstance.data) === null || _syncBlockInstance$da4 === void 0 ? void 0 : _syncBlockInstance$da4.product) !== null && _syncBlockInstance$da3 !== void 0 ? _syncBlockInstance$da3 : getSourceProductFromResourceIdSafe(syncBlockInstance.resourceId), buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), ((_syncBlockInstance$er3 = syncBlockInstance.error) === null || _syncBlockInstance$er3 === void 0 ? void 0 : _syncBlockInstance$er3.type) || ((_syncBlockInstance$er4 = syncBlockInstance.error) === null || _syncBlockInstance$er4 === void 0 ? void 0 : _syncBlockInstance$er4.reason), (_syncBlockInstance$er5 = syncBlockInstance.error) === null || _syncBlockInstance$er5 === void 0 ? void 0 : _syncBlockInstance$er5.statusCode)));
415
422
  }
416
423
  }
417
424
  }
@@ -58,34 +58,179 @@ export const buildErrorAttribution = (gateEnabled, error, statusCode) => {
58
58
  };
59
59
  };
60
60
 
61
+ /**
62
+ * The set of categorical failure reasons emitted on synced-block fetch/subscribe
63
+ * operational error events (EDITOR-7862). Extends the write-path
64
+ * {@link SyncBlockErrorReason} set with read-path-specific buckets so the analytics
65
+ * dashboard can break fetch failures down by cause instead of regex-matching the
66
+ * free-text `error` blob.
67
+ *
68
+ * The read path surfaces several causes the write-path `SyncBlockError` enum does not
69
+ * model (benign source-state transitions, permission denials, WebSocket lifecycle, and
70
+ * client-side readiness errors), so those are added here. Known `SyncBlockError` enum
71
+ * values (`not_found`, `forbidden`, ...) still pass through unchanged.
72
+ */
73
+
74
+ /**
75
+ * Fetch reasons that represent benign (working-as-designed) outcomes rather than genuine
76
+ * system failures. The dashboard uses this to compute a "true error rate" by excluding
77
+ * benign reasons via the structured `reason` attribute instead of brittle free-text regex
78
+ * (EDITOR-7862).
79
+ */
80
+ export const FETCH_BENIGN_REASONS = new Set(['source_deleted', 'source_unpublished', 'source_unsynced', 'source_not_found', 'permission_denied', 'unauthenticated',
81
+ // Mirror the benign write-path enum equivalents so already-classified errors are
82
+ // treated consistently.
83
+ 'not_found', 'forbidden', 'unpublished'
84
+ // NOTE: `entity_not_found` and `offline` are intentionally NOT benign.
85
+ // - `entity_not_found` is retried up to ENTITY_NOT_FOUND_MAX_RETRIES (analytics are
86
+ // suppressed during retries); by the time the error event fires, retries are
87
+ // exhausted and it is a genuine failure.
88
+ // - `offline` is genuine client connectivity loss, not a working-as-designed outcome.
89
+ // Both still emit `reason` (so they can be inspected) but are counted as true errors.
90
+ ]);
91
+
92
+ /**
93
+ * Ordered list of substring matchers mapping known free-text fetch/subscribe error
94
+ * strings to a categorical {@link SyncBlockFetchErrorReason}. Each entry is a list of
95
+ * lowercase needles; if ANY needle is found (case-insensitively) in the raw `error`
96
+ * string, the entry's reason is used. Order matters: more specific entries must precede
97
+ * more general ones.
98
+ *
99
+ * Plain `String.includes` substring matching is used deliberately instead of `RegExp`:
100
+ * it sidesteps the `require-unicode-regexp` lint rule AND the declaration build's lower
101
+ * compile target (which rejects the regex `u` flag with TS1501), while being faster and
102
+ * easier to read. Doing this classification in code (rather than in dashboard SQL) keeps
103
+ * the buckets versioned with the source strings that produce them (EDITOR-7862).
104
+ */
105
+ const FETCH_REASON_MATCHERS = [
106
+ // Benign: source-state transitions (deletionReason values + free text).
107
+ [['source-document-deleted'], 'source_deleted'], [['source-block-deleted'], 'source_deleted'], [['source-block-unpublished'], 'source_unpublished'], [['source-block-unsynced'], 'source_unsynced'], [['does not exist'], 'source_not_found'], [['unpublished'], 'source_unpublished'],
108
+ // Working-as-designed permission outcomes.
109
+ [['unauthenticated'], 'unauthenticated'], [['not permitted to read synced block'], 'permission_denied'], [['bulk permission check failed'], 'permission_denied'], [['permission denied'], 'permission_denied'],
110
+ // Genuine system failures.
111
+ [['reconnection failed after'], 'websocket_exhausted'],
112
+ // `reconnect to the subscription` (not a bare `reconnect`) avoids false positives on
113
+ // unrelated DB/GraphQL "reconnect" messages while still matching the WS-drop string.
114
+ [['websocket', 'reconnect to the subscription', 'subscription terminated'], 'websocket_drop'], [['429', 'rate limit', 'rate-limit', 'ratelimit'], 'rate_limited'],
115
+ // `econnrefused`/`econnreset` (not a bare `econn`, which would also match `reconnect`).
116
+ [['network', 'failed to fetch', 'econnrefused', 'econnreset', 'timed out', 'timeout'], 'network'], [['data provider not set'], 'data_provider_not_ready']];
117
+
118
+ /**
119
+ * Maps a fetch/subscribe `error` field — which may be a {@link SyncBlockError} enum
120
+ * value, a {@link DeletionReason} value, or an arbitrary free-text/JSON blob — to a
121
+ * stable categorical {@link SyncBlockFetchErrorReason} for analytics grouping.
122
+ *
123
+ * Resolution order:
124
+ * 1. Known `SyncBlockError` enum value → passed through (via {@link classifyErrorReason}).
125
+ * 2. Known free-text substring → mapped to a fetch-specific bucket.
126
+ * 3. Anything else (including opaque blobs like `errored`-only payloads or
127
+ * `ErrorEvent: "undefined"`) → `'unknown'`, so the dashboard never groups on free text.
128
+ *
129
+ * Note: the bare string `'errored'` IS a `SyncBlockError` enum value and therefore
130
+ * classifies as `'errored'` (not `'unknown'`); only genuinely unrecognised strings
131
+ * collapse to `'unknown'`.
132
+ */
133
+ export const classifyFetchErrorReason = error => {
134
+ const enumReason = classifyErrorReason(error);
135
+ if (enumReason !== 'unknown') {
136
+ return enumReason;
137
+ }
138
+ if (error) {
139
+ const haystack = error.toLowerCase();
140
+ for (const [needles, reason] of FETCH_REASON_MATCHERS) {
141
+ if (needles.some(needle => haystack.includes(needle))) {
142
+ return reason;
143
+ }
144
+ }
145
+ }
146
+ return 'unknown';
147
+ };
148
+
149
+ /**
150
+ * Extra, optional analytics attributes describing WHY a fetch/subscribe synced-block
151
+ * action failed. Spread conditionally so we never emit `undefined` keys (EDITOR-7862).
152
+ *
153
+ * Adds `benign` on top of the shared {@link ErrorAttributionAttributes} so the dashboard
154
+ * can compute a true error rate (`genuine / total`) without any free-text regex.
155
+ */
156
+
157
+ /**
158
+ * Builds the {@link FetchErrorAttributionAttributes} for a failed fetch/subscribe
159
+ * synced-block operation from the raw `error` field and optional backend `statusCode`.
160
+ * Returns `undefined` when the `platform_editor_blocks_patch_3` gate is OFF, so the new
161
+ * `reason`/`statusCode`/`benign` attributes are only emitted once the gate is rolled out
162
+ * (EDITOR-7862). The existing free-text `error` attribute is always left unchanged.
163
+ *
164
+ * `gateEnabled` is injected by the caller (the store managers evaluate `fg(...)`) so this
165
+ * helper stays pure and trivially unit-testable for both gate states.
166
+ */
167
+ export const buildFetchErrorAttribution = (gateEnabled, error, statusCode) => {
168
+ if (!gateEnabled) {
169
+ return undefined;
170
+ }
171
+ const reason = classifyFetchErrorReason(error);
172
+ return {
173
+ reason,
174
+ benign: FETCH_BENIGN_REASONS.has(reason),
175
+ ...(statusCode !== undefined && {
176
+ statusCode
177
+ })
178
+ };
179
+ };
180
+
61
181
  // `sourceProduct` is threaded through every helper so analytics
62
182
  // events can be attributed to the source product (`confluence-page` vs `jira-work-item`).
63
183
  // All helpers accept it as an optional trailing argument; the spread-only-when-truthy
64
184
  // pattern below ensures we never emit `sourceProduct: undefined` (which would dirty the
65
185
  // event schema with empty keys).
66
186
 
67
- export const getErrorPayload = (actionSubjectId, error, resourceId, sourceProduct, attribution) => ({
68
- action: ACTION.ERROR,
69
- actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
70
- actionSubjectId,
71
- eventType: EVENT_TYPE.OPERATIONAL,
72
- attributes: {
73
- error,
74
- ...(resourceId && {
75
- resourceId
76
- }),
77
- ...(sourceProduct && {
78
- sourceProduct
79
- }),
80
- ...((attribution === null || attribution === void 0 ? void 0 : attribution.reason) && {
81
- reason: attribution.reason
82
- }),
83
- ...((attribution === null || attribution === void 0 ? void 0 : attribution.statusCode) !== undefined && {
84
- statusCode: attribution.statusCode
85
- })
86
- }
87
- });
88
- export const fetchErrorPayload = (error, resourceId, sourceProduct) => getErrorPayload(ACTION_SUBJECT_ID.SYNCED_BLOCK_FETCH, error, resourceId, sourceProduct);
187
+ /**
188
+ * Shared operational ERROR payload builder for synced-block events.
189
+ *
190
+ * Overloaded so the emitted `reason` type stays accurate per path (raised in review on
191
+ * EDITOR-7862): fetch/subscribe callers pass a {@link FetchErrorAttributionAttributes}
192
+ * (discriminated by its required `benign` field) and get the wider
193
+ * {@link SyncBlockFetchErrorReason}; write-path callers pass an
194
+ * {@link ErrorAttributionAttributes} (or nothing) and keep the narrower
195
+ * {@link SyncBlockErrorReason}, so write-path payloads never claim to carry fetch-only
196
+ * buckets like `source_deleted` they never emit.
197
+ */
198
+
199
+ export function getErrorPayload(actionSubjectId, error, resourceId, sourceProduct, attribution) {
200
+ return {
201
+ action: ACTION.ERROR,
202
+ actionSubject: ACTION_SUBJECT.SYNCED_BLOCK,
203
+ actionSubjectId,
204
+ eventType: EVENT_TYPE.OPERATIONAL,
205
+ attributes: {
206
+ error,
207
+ ...(resourceId && {
208
+ resourceId
209
+ }),
210
+ ...(sourceProduct && {
211
+ sourceProduct
212
+ }),
213
+ ...((attribution === null || attribution === void 0 ? void 0 : attribution.reason) && {
214
+ reason: attribution.reason
215
+ }),
216
+ ...((attribution === null || attribution === void 0 ? void 0 : attribution.statusCode) !== undefined && {
217
+ statusCode: attribution.statusCode
218
+ }),
219
+ // `benign` is only present on fetch/subscribe attribution (EDITOR-7862). It is a
220
+ // boolean (never UGC); the `'benign' in attribution` check both guards the
221
+ // write-path attribution (which has no `benign`) and narrows the union so
222
+ // `attribution.benign` is accessible.
223
+ ...(attribution && 'benign' in attribution && {
224
+ benign: attribution.benign
225
+ })
226
+ }
227
+ };
228
+ }
229
+ export const fetchErrorPayload = (error, resourceId, sourceProduct, attribution) =>
230
+ // Branch on attribution presence so each call resolves to a concrete overload: with
231
+ // attribution it hits the fetch overload (wider `reason`); without it (gate OFF) it
232
+ // hits the no-attribution overload. Both produce a fetch event regardless.
233
+ attribution ? getErrorPayload(ACTION_SUBJECT_ID.SYNCED_BLOCK_FETCH, error, resourceId, sourceProduct, attribution) : getErrorPayload(ACTION_SUBJECT_ID.SYNCED_BLOCK_FETCH, error, resourceId, sourceProduct);
89
234
  export const getSourceInfoErrorPayload = (error, resourceId, sourceProduct) => getErrorPayload(ACTION_SUBJECT_ID.SYNCED_BLOCK_GET_SOURCE_INFO, error, resourceId, sourceProduct);
90
235
  export const updateErrorPayload = (error, resourceId, sourceProduct, attribution) => getErrorPayload(ACTION_SUBJECT_ID.SYNCED_BLOCK_UPDATE, error, resourceId, sourceProduct, attribution);
91
236
  export const updateReferenceErrorPayload = (error, resourceId, sourceProduct, attribution) => getErrorPayload(ACTION_SUBJECT_ID.REFERENCE_SYNCED_BLOCK_UPDATE, error, resourceId, sourceProduct, attribution);
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable require-unicode-regexp */
2
2
 
3
+ const GET_CONTENT_ID_AND_PRODUCT_REGEX = /^(confluence-page|jira-work-item)\/([^/]+)/;
3
4
  const normalizeSyncBlockJSONContentInternal = (content, options) => {
4
5
  var _normalizedContent3;
5
6
  let normalizedContent;
@@ -94,7 +95,7 @@ export const convertPMNodesToSyncBlockNodes = nodes => {
94
95
  * Extracts the source page content id and source product
95
96
  */
96
97
  export const getContentIdAndProductFromResourceId = resourceId => {
97
- const match = resourceId.match(/^(confluence-page|jira-work-item)\/([^/]+)/);
98
+ const match = resourceId.match(GET_CONTENT_ID_AND_PRODUCT_REGEX);
98
99
  if (match !== null && match !== void 0 && match[2]) {
99
100
  return {
100
101
  sourceProduct: match[1],
@@ -1,5 +1,9 @@
1
1
  /* eslint-disable require-unicode-regexp */
2
2
 
3
+ var GET_LOCAL_ID_FROM_BLOCK_RESOURCE_ID_REGEX = /ari:cloud:blocks:[^:]+:synced-block\/([a-zA-Z0-9-]+)/;
4
+ var JIRA_SOURCE_ARI_REGEX = /ari:cloud:jira:.*/;
5
+ var CONFLUENCE_SOURCE_ARI_REGEX = /ari:cloud:confluence:.*/;
6
+
3
7
  /**
4
8
  * Generates the block ARI from the source page ARI and the source block's resource ID.
5
9
  * @param cloudId - the cloudId of the block. E.G the cloudId of the confluence page, or the cloudId of the Jira instance
@@ -36,18 +40,18 @@ export var generateBlockAriFromReference = function generateBlockAriFromReferenc
36
40
  * @returns the localId of the block node. A randomly generated UUID
37
41
  */
38
42
  export var getLocalIdFromBlockResourceId = function getLocalIdFromBlockResourceId(ari) {
39
- var match = ari.match(/ari:cloud:blocks:[^:]+:synced-block\/([a-zA-Z0-9-]+)/);
43
+ var match = ari.match(GET_LOCAL_ID_FROM_BLOCK_RESOURCE_ID_REGEX);
40
44
  if (match !== null && match !== void 0 && match[1]) {
41
45
  return match[1];
42
46
  }
43
47
  throw new Error("Invalid block ARI: ".concat(ari));
44
48
  };
45
49
  export var getProductFromSourceAri = function getProductFromSourceAri(ari) {
46
- var jiraMatch = ari === null || ari === void 0 ? void 0 : ari.search(/ari:cloud:jira:.*/);
50
+ var jiraMatch = ari === null || ari === void 0 ? void 0 : ari.search(JIRA_SOURCE_ARI_REGEX);
47
51
  if (jiraMatch !== -1) {
48
52
  return 'jira-work-item';
49
53
  }
50
- var confluenceMatch = ari === null || ari === void 0 ? void 0 : ari.search(/ari:cloud:confluence:.*/);
54
+ var confluenceMatch = ari === null || ari === void 0 ? void 0 : ari.search(CONFLUENCE_SOURCE_ARI_REGEX);
51
55
  if (confluenceMatch !== -1) {
52
56
  return 'confluence-page';
53
57
  }
@@ -3,6 +3,9 @@ import { createClient } from 'graphql-ws';
3
3
  import { isSSR } from '@atlaskit/editor-common/core-utils';
4
4
  import { convertContentUpdatedAt } from '../../utils/utils';
5
5
  var GRAPHQL_WS_ENDPOINT = '/gateway/api/graphql/subscriptions';
6
+
7
+ // eslint-disable-next-line require-unicode-regexp
8
+ var EXTRACT_RESOURCE_ID_FROM_BLOCK_ARI_REGEX = /ari:cloud:blocks:[^:]+:synced-block\/(.+)$/;
6
9
  var blockServiceClient = null;
7
10
 
8
11
  /**
@@ -122,8 +125,7 @@ export var extractGraphQLWSErrorMessage = function extractGraphQLWSErrorMessage(
122
125
  * @returns The resourceId portion of the ARI
123
126
  */
124
127
  var extractResourceIdFromBlockAri = function extractResourceIdFromBlockAri(blockAri) {
125
- // eslint-disable-next-line require-unicode-regexp
126
- var match = blockAri.match(/ari:cloud:blocks:[^:]+:synced-block\/(.+)$/);
128
+ var match = blockAri.match(EXTRACT_RESOURCE_ID_FROM_BLOCK_ARI_REGEX);
127
129
  return (match === null || match === void 0 ? void 0 : match[1]) || null;
128
130
  };
129
131
 
@@ -1,5 +1,7 @@
1
1
  /* eslint-disable require-unicode-regexp */
2
2
 
3
+ var CONFLUENCE_PAGE_ARI_REGEX = /ari:cloud:confluence:[^:]+:(page|blogpost)\/(\d+)/;
4
+
3
5
  /**
4
6
  * The type of the Confluence page
5
7
  */
@@ -25,7 +27,7 @@ export var getConfluencePageAri = function getConfluencePageAri(_ref) {
25
27
  */
26
28
  export var getPageIdAndTypeFromConfluencePageAri = function getPageIdAndTypeFromConfluencePageAri(_ref2) {
27
29
  var ari = _ref2.ari;
28
- var match = ari.match(/ari:cloud:confluence:[^:]+:(page|blogpost)\/(\d+)/);
30
+ var match = ari.match(CONFLUENCE_PAGE_ARI_REGEX);
29
31
  if (match !== null && match !== void 0 && match[2]) {
30
32
  return {
31
33
  type: match[1],
@@ -1,5 +1,7 @@
1
1
  /* eslint-disable require-unicode-regexp */
2
2
 
3
+ var JIRA_WORK_ITEM_ARI_REGEX = /ari:cloud:jira:([^:]+):issue\/(\d+)/;
4
+
3
5
  /**
4
6
  * Generates the Jira work item ARI
5
7
  * @param workItemId - the ID of the work item
@@ -19,7 +21,7 @@ export var getJiraWorkItemAri = function getJiraWorkItemAri(_ref) {
19
21
  */
20
22
  export var getJiraWorkItemIdFromAri = function getJiraWorkItemIdFromAri(_ref2) {
21
23
  var ari = _ref2.ari;
22
- var match = ari.match(/ari:cloud:jira:([^:]+):issue\/(\d+)/);
24
+ var match = ari.match(JIRA_WORK_ITEM_ARI_REGEX);
23
25
  if (match !== null && match !== void 0 && match[2]) {
24
26
  return match[2];
25
27
  }
@@ -9,7 +9,7 @@ import { isSSR } from '@atlaskit/editor-common/core-utils';
9
9
  import { logException } from '@atlaskit/editor-common/monitoring';
10
10
  import { fg } from '@atlaskit/platform-feature-flags';
11
11
  import { isProviderNotReadyError, SyncBlockError } from '../common/types';
12
- import { fetchErrorPayload } from '../utils/errorHandling';
12
+ import { buildFetchErrorAttribution, fetchErrorPayload } from '../utils/errorHandling';
13
13
  import { createSyncBlockNode, getSourceProductFromResourceIdSafe } from '../utils/utils';
14
14
  export var useFetchSyncBlockData = function useFetchSyncBlockData(manager, resourceId, localId, fireAnalyticsEvent) {
15
15
  var _manager$referenceMan2, _manager$referenceMan3, _manager$referenceMan4;
@@ -98,7 +98,7 @@ export var useFetchSyncBlockData = function useFetchSyncBlockData(manager, resou
98
98
  logException(_t, {
99
99
  location: 'editor-synced-block-provider/useFetchSyncBlockData'
100
100
  });
101
- fireAnalyticsEvent === null || fireAnalyticsEvent === void 0 || fireAnalyticsEvent(fetchErrorPayload(_t.message, resourceId, getSourceProductFromResourceIdSafe(resourceId)));
101
+ fireAnalyticsEvent === null || fireAnalyticsEvent === void 0 || fireAnalyticsEvent(fetchErrorPayload(_t.message, resourceId, getSourceProductFromResourceIdSafe(resourceId), buildFetchErrorAttribution(fg('platform_editor_blocks_patch_3'), _t.message)));
102
102
 
103
103
  // Set error state if fetching fails
104
104
  setFetchState({