@atlaskit/media-card 79.11.4 → 79.13.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 (47) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cjs/card/card.js +1 -1
  3. package/dist/cjs/card/externalImageCard.js +17 -12
  4. package/dist/cjs/card/fileCard.js +34 -12
  5. package/dist/cjs/card/media-card-analytics-error-boundary.js +1 -1
  6. package/dist/cjs/card/ui/tickBox/tickBoxWrapper-compiled.compiled.css +0 -3
  7. package/dist/cjs/card/ui/tickBox/tickBoxWrapper-compiled.js +1 -3
  8. package/dist/cjs/card/ui/titleBox/failedTitleBox.js +0 -2
  9. package/dist/cjs/card/ui/titleBox/titleBoxComponents-compiled.js +1 -3
  10. package/dist/cjs/inline/loader.js +1 -1
  11. package/dist/cjs/inline/mediaInlineAnalyticsErrorBoundary.js +0 -2
  12. package/dist/cjs/utils/globalScope/globalScope.js +2 -1
  13. package/dist/cjs/utils/mediaPerformanceObserver/durationMetrics.js +1 -0
  14. package/dist/cjs/utils/ufoExperiences.js +454 -68
  15. package/dist/es2019/card/card.js +1 -1
  16. package/dist/es2019/card/externalImageCard.js +16 -13
  17. package/dist/es2019/card/fileCard.js +33 -13
  18. package/dist/es2019/card/media-card-analytics-error-boundary.js +1 -1
  19. package/dist/es2019/card/ui/tickBox/tickBoxWrapper-compiled.compiled.css +0 -3
  20. package/dist/es2019/card/ui/tickBox/tickBoxWrapper-compiled.js +1 -3
  21. package/dist/es2019/card/ui/titleBox/failedTitleBox.js +0 -2
  22. package/dist/es2019/card/ui/titleBox/titleBoxComponents-compiled.js +1 -3
  23. package/dist/es2019/inline/loader.js +1 -1
  24. package/dist/es2019/inline/mediaInlineAnalyticsErrorBoundary.js +0 -2
  25. package/dist/es2019/utils/globalScope/globalScope.js +1 -0
  26. package/dist/es2019/utils/mediaPerformanceObserver/durationMetrics.js +1 -0
  27. package/dist/es2019/utils/ufoExperiences.js +449 -72
  28. package/dist/esm/card/card.js +1 -1
  29. package/dist/esm/card/externalImageCard.js +18 -13
  30. package/dist/esm/card/fileCard.js +35 -13
  31. package/dist/esm/card/media-card-analytics-error-boundary.js +1 -1
  32. package/dist/esm/card/ui/tickBox/tickBoxWrapper-compiled.compiled.css +0 -3
  33. package/dist/esm/card/ui/tickBox/tickBoxWrapper-compiled.js +1 -3
  34. package/dist/esm/card/ui/titleBox/failedTitleBox.js +0 -2
  35. package/dist/esm/card/ui/titleBox/titleBoxComponents-compiled.js +1 -3
  36. package/dist/esm/inline/loader.js +1 -1
  37. package/dist/esm/inline/mediaInlineAnalyticsErrorBoundary.js +0 -2
  38. package/dist/esm/utils/globalScope/globalScope.js +1 -0
  39. package/dist/esm/utils/mediaPerformanceObserver/durationMetrics.js +1 -0
  40. package/dist/esm/utils/ufoExperiences.js +454 -68
  41. package/dist/types/utils/globalScope/globalScope.d.ts +2 -0
  42. package/dist/types/utils/mediaPerformanceObserver/durationMetrics.d.ts +1 -0
  43. package/dist/types/utils/ufoExperiences.d.ts +88 -5
  44. package/dist/types-ts4.5/utils/globalScope/globalScope.d.ts +2 -0
  45. package/dist/types-ts4.5/utils/mediaPerformanceObserver/durationMetrics.d.ts +1 -0
  46. package/dist/types-ts4.5/utils/ufoExperiences.d.ts +88 -5
  47. package/package.json +9 -9
@@ -1,12 +1,386 @@
1
- import { ConcurrentExperience, ExperiencePerformanceTypes, ExperienceTypes } from '@atlaskit/ufo';
1
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import { UFOExperience, ExperiencePerformanceTypes, ExperienceTypes, ConcurrentExperience } from '@atlaskit/ufo';
2
3
  import { getFeatureFlagKeysAllProducts } from '@atlaskit/media-common';
3
4
  import isValidId from 'uuid-validate';
4
5
  import { extractErrorInfo, getRenderErrorRequestMetadata } from './analytics';
5
6
  import { MediaCardError } from '../errors';
6
7
  import { getMediaEnvironment, getMediaRegion } from '@atlaskit/media-client';
8
+ import { getActiveInteraction } from '@atlaskit/react-ufo/interaction-metrics';
9
+ import { getMediaGlobalScope } from './globalScope/globalScope';
7
10
  const packageName = "@atlaskit/media-card";
8
- const packageVersion = "79.11.3";
11
+ const packageVersion = "79.13.0";
9
12
  const SAMPLE_RATE = 0.05;
13
+
14
+ /**
15
+ * Determines if performance events should be sampled for this instance.
16
+ * Approximately 5% of instances will be sampled.
17
+ */
18
+ export const shouldPerformanceBeSampled = () => Math.random() < SAMPLE_RATE;
19
+
20
+ /**
21
+ * Gets the UFO interaction start time.
22
+ * For page_load: returns 0 (relative to page navigation)
23
+ * For SPA transitions: returns performance.now() when transition started
24
+ */
25
+ const getInteractionStartTime = () => {
26
+ var _interaction$start;
27
+ const interaction = getActiveInteraction();
28
+ return (_interaction$start = interaction === null || interaction === void 0 ? void 0 : interaction.start) !== null && _interaction$start !== void 0 ? _interaction$start : 0;
29
+ };
30
+
31
+ /**
32
+ * Finds a performance resource timing entry by matching a full URI.
33
+ * Searches from the end (most recent) to find the latest matching entry.
34
+ */
35
+ const findPerformanceEntryByName = name => {
36
+ var _getMediaGlobalScope$;
37
+ if (typeof performance === 'undefined' || !performance.getEntriesByType) {
38
+ return undefined;
39
+ }
40
+
41
+ // For data URIs (base64), there won't be a performance entry
42
+ if (name.startsWith('data:')) {
43
+ return undefined;
44
+ }
45
+ const entries = performance.getEntriesByType('resource');
46
+ const ssrPerformanceEntries = (_getMediaGlobalScope$ = getMediaGlobalScope().performanceEntries) !== null && _getMediaGlobalScope$ !== void 0 ? _getMediaGlobalScope$ : [];
47
+ return [...ssrPerformanceEntries, ...entries].find(entry => name.includes(entry.name));
48
+ };
49
+
50
+ /**
51
+ * Creates timing configuration for the UFO experience based on the performance entry.
52
+ * These timings will be calculated by UFO using the marks we add.
53
+ */
54
+ const createTimingsConfig = prefix => [{
55
+ key: `${prefix}:resourceTiming`,
56
+ startMark: `${prefix}:resourceTiming:start`,
57
+ endMark: `${prefix}:resourceTiming:end`
58
+ }, {
59
+ key: `${prefix}:dnsLookup`,
60
+ startMark: `${prefix}:dnsLookup:start`,
61
+ endMark: `${prefix}:dnsLookup:end`
62
+ }, {
63
+ key: `${prefix}:tcpHandshake`,
64
+ startMark: `${prefix}:tcpHandshake:start`,
65
+ endMark: `${prefix}:tcpHandshake:end`
66
+ }, {
67
+ key: `${prefix}:tlsNegotiation`,
68
+ startMark: `${prefix}:tlsNegotiation:start`,
69
+ endMark: `${prefix}:tlsNegotiation:end`
70
+ }, {
71
+ key: `${prefix}:ttfb`,
72
+ startMark: `${prefix}:ttfb:start`,
73
+ endMark: `${prefix}:ttfb:end`
74
+ }, {
75
+ key: `${prefix}:contentDownload`,
76
+ startMark: `${prefix}:contentDownload:start`,
77
+ endMark: `${prefix}:contentDownload:end`
78
+ }, {
79
+ key: `${prefix}:redirect`,
80
+ startMark: `${prefix}:redirect:start`,
81
+ endMark: `${prefix}:redirect:end`
82
+ }, {
83
+ key: `${prefix}:fetchTime`,
84
+ startMark: `${prefix}:fetchTime:start`,
85
+ endMark: `${prefix}:fetchTime:end`
86
+ }];
87
+
88
+ /**
89
+ * Creates resource timing metadata from a performance entry.
90
+ * Only includes non-timing information (sizes, cache status, protocol, CDN info).
91
+ * Timing durations are captured via marks which feed into the timings config.
92
+ */
93
+ const createResourceTimingMetadata = entry => {
94
+ var _entry$serverTiming$s, _entry$serverTiming, _entry$serverTiming2, _entry$serverTiming2$, _entry$serverTiming3, _entry$serverTiming3$;
95
+ return {
96
+ // Size information
97
+ resourceTransferSize: entry.transferSize,
98
+ resourceDecodedBodySize: entry.decodedBodySize,
99
+ // Request info
100
+ resourceInitiatorType: entry.initiatorType,
101
+ resourceNextHopProtocol: entry.nextHopProtocol,
102
+ // Cache status
103
+ resourceBrowserCacheHit: entry.transferSize === 0,
104
+ // Server timing (CDN metrics)
105
+ resourceCdnCacheHit: (_entry$serverTiming$s = (_entry$serverTiming = entry.serverTiming) === null || _entry$serverTiming === void 0 ? void 0 : _entry$serverTiming.some(({
106
+ name
107
+ }) => name === 'cdn-cache-hit')) !== null && _entry$serverTiming$s !== void 0 ? _entry$serverTiming$s : false,
108
+ resourceCdnDownstreamFBL: (_entry$serverTiming2 = entry.serverTiming) === null || _entry$serverTiming2 === void 0 ? void 0 : (_entry$serverTiming2$ = _entry$serverTiming2.find(({
109
+ name
110
+ }) => name === 'cdn-downstream-fbl')) === null || _entry$serverTiming2$ === void 0 ? void 0 : _entry$serverTiming2$.duration,
111
+ resourceCdnUpstreamFBL: (_entry$serverTiming3 = entry.serverTiming) === null || _entry$serverTiming3 === void 0 ? void 0 : (_entry$serverTiming3$ = _entry$serverTiming3.find(({
112
+ name
113
+ }) => name === 'cdn-upstream-fbl')) === null || _entry$serverTiming3$ === void 0 ? void 0 : _entry$serverTiming3$.duration
114
+ };
115
+ };
116
+
117
+ /**
118
+ * Adds timing marks from a performance resource timing entry to the UFO experience.
119
+ */
120
+ const addResourceTimingMarks = (experience, entry, interactionStartTime, prefix) => {
121
+ const addMark = (name, start, end) => {
122
+ const relativeStart = start - interactionStartTime;
123
+ const relativeEnd = end - interactionStartTime;
124
+ if (relativeEnd > relativeStart && relativeStart >= 0) {
125
+ experience.mark(`${prefix}:${name}:start`, relativeStart);
126
+ experience.mark(`${prefix}:${name}:end`, relativeEnd);
127
+ }
128
+ };
129
+
130
+ // Overall resource timing
131
+ addMark('resourceTiming', entry.startTime, entry.responseEnd);
132
+
133
+ // DNS lookup
134
+ addMark('dnsLookup', entry.domainLookupStart, entry.domainLookupEnd);
135
+
136
+ // TCP handshake
137
+ addMark('tcpHandshake', entry.connectStart, entry.connectEnd);
138
+
139
+ // TLS negotiation (only for HTTPS)
140
+ if (entry.secureConnectionStart > 0) {
141
+ addMark('tlsNegotiation', entry.secureConnectionStart, entry.requestStart);
142
+ }
143
+
144
+ // Request to first byte (TTFB)
145
+ addMark('ttfb', entry.requestStart, entry.responseStart);
146
+
147
+ // Content download
148
+ addMark('contentDownload', entry.responseStart, entry.responseEnd);
149
+
150
+ // Redirect time (if any)
151
+ if (entry.redirectStart > 0 && entry.redirectEnd > 0) {
152
+ addMark('redirect', entry.redirectStart, entry.redirectEnd);
153
+ }
154
+
155
+ // Total fetch time (without redirect) - from fetchStart to responseEnd
156
+ addMark('fetchTime', entry.fetchStart, entry.responseEnd);
157
+ };
158
+ const sanitiseFileAttributes = fileAttributes => {
159
+ let sanitisedFileId = 'INVALID_FILE_ID';
160
+ if (fileAttributes.fileId === 'external-image' || isValidId(fileAttributes.fileId)) {
161
+ sanitisedFileId = fileAttributes.fileId;
162
+ }
163
+ return {
164
+ ...fileAttributes,
165
+ fileId: sanitisedFileId
166
+ };
167
+ };
168
+ const getBasePayloadAttributes = () => ({
169
+ packageName,
170
+ packageVersion,
171
+ mediaEnvironment: getMediaEnvironment(),
172
+ mediaRegion: getMediaRegion()
173
+ });
174
+ /**
175
+ * Creates a new UFO experience instance with the given configuration.
176
+ */
177
+ const createExperience = (instanceId, timings = []) => {
178
+ return new UFOExperience('media-card-render', {
179
+ platform: {
180
+ component: 'media-card'
181
+ },
182
+ type: ExperienceTypes.Experience,
183
+ performanceType: ExperiencePerformanceTypes.InlineResult,
184
+ featureFlags: getFeatureFlagKeysAllProducts(),
185
+ timings
186
+ }, instanceId);
187
+ };
188
+
189
+ /**
190
+ * Hook to create a UFO experience tied to a media card component lifecycle.
191
+ *
192
+ * This creates a unique UFOExperience instance per component, allowing:
193
+ * - Unique timing config per instance
194
+ * - Direct control over experience lifecycle
195
+ * - Proper cleanup on unmount
196
+ *
197
+ * @example
198
+ * ```tsx
199
+ * const ufoExperience = useMediaCardUfoExperience({
200
+ * instanceId: internalOccurrenceKey,
201
+ * enabled: shouldSendPerformanceEvent,
202
+ * });
203
+ *
204
+ * // On card visible
205
+ * ufoExperience.start();
206
+ *
207
+ * // On card complete/error
208
+ * ufoExperience.complete(status, fileAttributes, fileStateFlags, ssrReliability, error, ssrPreviewInfo);
209
+ * ```
210
+ */
211
+ export const useMediaCardUfoExperience = ({
212
+ instanceId,
213
+ enabled
214
+ }) => {
215
+ // Store the start time when start() is called - experience creation is deferred to complete()
216
+ const startTimeRef = useRef(undefined);
217
+ const hasStartedRef = useRef(false);
218
+ // Store the experience so abort() can use it after complete() has run
219
+ const experienceRef = useRef(null);
220
+
221
+ // Reset refs when instanceId changes (new card instance)
222
+ useEffect(() => {
223
+ return () => {
224
+ // Note: Don't clear experienceRef here as abort() might still need it
225
+ // The component's cleanup calls abort() which handles final state
226
+ hasStartedRef.current = false;
227
+ startTimeRef.current = undefined;
228
+ };
229
+ }, [instanceId]);
230
+ const start = useCallback(options => {
231
+ if (!enabled) {
232
+ return;
233
+ }
234
+ // Store the start time - experience will be created in complete()
235
+ // This allows us to have the correct timings config when creating the experience
236
+ hasStartedRef.current = true;
237
+ startTimeRef.current = options !== null && options !== void 0 && options.useInteractionTime ? getInteractionStartTime() : performance.now();
238
+ }, [enabled]);
239
+ const complete = useCallback((status, fileAttributes, fileStateFlags, ssrReliability, error = new MediaCardError('missing-error-data'), ssrPreviewInfo) => {
240
+ // Only complete for terminal statuses - ignore intermediate statuses like 'loading-preview'
241
+ if (!['complete', 'error', 'failed-processing'].includes(status)) {
242
+ return;
243
+ }
244
+ if (!enabled || !hasStartedRef.current) {
245
+ return;
246
+ }
247
+ const interactionStartTime = getInteractionStartTime();
248
+
249
+ // Determine timings config based on SSR status
250
+ let timingsConfig = [];
251
+ let resourceTimingEntry;
252
+ if (ssrPreviewInfo !== null && ssrPreviewInfo !== void 0 && ssrPreviewInfo.wasSSRSuccessful && ssrPreviewInfo.dataUri) {
253
+ var _ssrPreviewInfo$srcse;
254
+ resourceTimingEntry = findPerformanceEntryByName((_ssrPreviewInfo$srcse = ssrPreviewInfo.srcset) !== null && _ssrPreviewInfo$srcse !== void 0 ? _ssrPreviewInfo$srcse : ssrPreviewInfo.dataUri);
255
+ if (resourceTimingEntry) {
256
+ timingsConfig = createTimingsConfig('ssr');
257
+ }
258
+ }
259
+
260
+ // Create the experience with the correct timings config
261
+ const experience = createExperience(instanceId, timingsConfig);
262
+ experienceRef.current = experience;
263
+ experience.start(startTimeRef.current);
264
+
265
+ // Add timing marks and metadata based on strategy
266
+ if (ssrPreviewInfo !== null && ssrPreviewInfo !== void 0 && ssrPreviewInfo.wasSSRSuccessful && ssrPreviewInfo.dataUri) {
267
+ if (resourceTimingEntry) {
268
+ addResourceTimingMarks(experience, resourceTimingEntry, interactionStartTime, 'ssr');
269
+ experience.addMetadata({
270
+ ...createResourceTimingMetadata(resourceTimingEntry),
271
+ timingStrategy: 'ssr-resource-timing'
272
+ });
273
+ } else {
274
+ experience.addMetadata({
275
+ timingStrategy: 'ssr-no-entry-found'
276
+ });
277
+ }
278
+ } else if (ssrPreviewInfo !== null && ssrPreviewInfo !== void 0 && ssrPreviewInfo.wasSSRAttempted) {
279
+ // Strategy 2: SSR was attempted but failed - use interaction start time
280
+ experience.addMetadata({
281
+ timingStrategy: 'ssr-failed',
282
+ interactionStartTime
283
+ });
284
+ experience.mark('interactionStart', 0);
285
+ } else {
286
+ // Strategy 3: No SSR - CSR mount-based behavior
287
+ experience.addMetadata({
288
+ timingStrategy: 'csr-mount-based'
289
+ });
290
+ }
291
+
292
+ // Complete the experience with appropriate state
293
+ const sanitisedFileAttributes = sanitiseFileAttributes(fileAttributes);
294
+ switch (status) {
295
+ case 'complete':
296
+ experience.success({
297
+ metadata: {
298
+ fileAttributes: sanitisedFileAttributes,
299
+ ssrReliability,
300
+ fileStateFlags,
301
+ ...getBasePayloadAttributes()
302
+ }
303
+ });
304
+ break;
305
+ case 'failed-processing':
306
+ experience.failure({
307
+ metadata: {
308
+ fileAttributes: sanitisedFileAttributes,
309
+ ssrReliability,
310
+ fileStateFlags,
311
+ failReason: 'failed-processing',
312
+ ...getBasePayloadAttributes()
313
+ }
314
+ });
315
+ break;
316
+ case 'error':
317
+ experience.failure({
318
+ metadata: {
319
+ fileAttributes: sanitisedFileAttributes,
320
+ ssrReliability,
321
+ fileStateFlags,
322
+ ...extractErrorInfo(error),
323
+ request: getRenderErrorRequestMetadata(error),
324
+ ...getBasePayloadAttributes()
325
+ }
326
+ });
327
+ break;
328
+ }
329
+
330
+ // Reset state after completion
331
+ hasStartedRef.current = false;
332
+ startTimeRef.current = undefined;
333
+ }, [enabled, instanceId]);
334
+ const abort = useCallback(properties => {
335
+ if (!enabled) {
336
+ return;
337
+ }
338
+
339
+ // Use existing experience if available (created by complete()),
340
+ // otherwise create new one if experience was started but not completed
341
+ let experience = experienceRef.current;
342
+ if (!experience) {
343
+ if (!hasStartedRef.current) {
344
+ return;
345
+ }
346
+ experience = createExperience(instanceId);
347
+ experienceRef.current = experience;
348
+ experience.start(startTimeRef.current);
349
+ }
350
+ const metadata = {
351
+ ...getBasePayloadAttributes()
352
+ };
353
+ if (properties !== null && properties !== void 0 && properties.fileAttributes) {
354
+ metadata.fileAttributes = sanitiseFileAttributes(properties.fileAttributes);
355
+ }
356
+ if (properties !== null && properties !== void 0 && properties.fileStateFlags) {
357
+ metadata.fileStateFlags = properties.fileStateFlags;
358
+ }
359
+ if (properties !== null && properties !== void 0 && properties.ssrReliability) {
360
+ metadata.ssrReliability = properties.ssrReliability;
361
+ }
362
+
363
+ // UFO will ignore abort if experience is already in final state
364
+ experience.abort({
365
+ metadata
366
+ });
367
+
368
+ // Reset state after abort
369
+ hasStartedRef.current = false;
370
+ startTimeRef.current = undefined;
371
+ }, [enabled, instanceId]);
372
+ return useMemo(() => ({
373
+ start,
374
+ complete,
375
+ abort
376
+ }), [start, complete, abort]);
377
+ };
378
+
379
+ // ============================================================================
380
+ // Legacy exports for backwards compatibility
381
+ // These will be removed once all consumers are migrated to the hook
382
+ // ============================================================================
383
+
10
384
  let concurrentExperience;
11
385
  const getExperience = id => {
12
386
  if (!concurrentExperience) {
@@ -22,93 +396,96 @@ const getExperience = id => {
22
396
  }
23
397
  return concurrentExperience.getInstance(id);
24
398
  };
25
- export const shouldPerformanceBeSampled = () =>
26
- // We generate about 100M events UFOv1 events, we want to reduce this to about 5M as we can get the same info from there
27
- // Math.random() generates a random floating-point number between 0 (inclusive) and 1 (exclusive).
28
- // The condition Math.random() < SAMPLE_RATE (0.05) will be true approximately 5% of the time.
29
- Math.random() < SAMPLE_RATE;
30
- export const startUfoExperience = id => {
31
- getExperience(id).start();
399
+ export const startUfoExperience = (id, startTime) => {
400
+ getExperience(id).start(startTime);
32
401
  };
33
- const sanitiseFileAttributes = fileAttributes => {
34
- /*
35
- Allow external image mediaItemType as fileId
36
- See ExternalImageIdentifier interface on platform/packages/media/media-client/src/identifier.ts
37
- */
38
- let sanitisedFileId = 'INVALID_FILE_ID';
39
- if (fileAttributes.fileId === 'external-image' || isValidId(fileAttributes.fileId)) {
40
- sanitisedFileId = fileAttributes.fileId;
402
+ export const completeUfoExperience = (id, status, fileAttributes, fileStateFlags, ssrReliability, error = new MediaCardError('missing-error-data'), ssrPreviewInfo) => {
403
+ // Only complete for terminal statuses - ignore intermediate statuses like 'loading-preview'
404
+ if (!['complete', 'error', 'failed-processing'].includes(status)) {
405
+ return;
41
406
  }
42
- return {
43
- ...fileAttributes,
44
- fileId: sanitisedFileId
45
- };
46
- };
47
- export const completeUfoExperience = (id, status, fileAttributes, fileStateFlags, ssrReliability, error = new MediaCardError('missing-error-data')) => {
407
+ const experience = getExperience(id);
408
+ const interactionStartTime = getInteractionStartTime();
409
+
410
+ // Determine timing strategy based on SSR status
411
+ if (ssrPreviewInfo !== null && ssrPreviewInfo !== void 0 && ssrPreviewInfo.wasSSRSuccessful && ssrPreviewInfo.dataUri) {
412
+ var _ssrPreviewInfo$srcse2;
413
+ const entry = findPerformanceEntryByName((_ssrPreviewInfo$srcse2 = ssrPreviewInfo.srcset) !== null && _ssrPreviewInfo$srcse2 !== void 0 ? _ssrPreviewInfo$srcse2 : ssrPreviewInfo.dataUri);
414
+ if (entry) {
415
+ addResourceTimingMarks(experience, entry, interactionStartTime, 'ssr');
416
+ experience.addMetadata({
417
+ ...createResourceTimingMetadata(entry),
418
+ timingStrategy: 'ssr-resource-timing'
419
+ });
420
+ } else {
421
+ experience.addMetadata({
422
+ timingStrategy: 'ssr-no-entry-found'
423
+ });
424
+ }
425
+ } else if (ssrPreviewInfo !== null && ssrPreviewInfo !== void 0 && ssrPreviewInfo.wasSSRAttempted) {
426
+ experience.addMetadata({
427
+ timingStrategy: 'ssr-failed',
428
+ interactionStartTime
429
+ });
430
+ experience.mark('interactionStart', 0);
431
+ } else {
432
+ experience.addMetadata({
433
+ timingStrategy: 'csr-mount-based'
434
+ });
435
+ }
436
+ const sanitisedFileAttributes = sanitiseFileAttributes(fileAttributes);
48
437
  switch (status) {
49
438
  case 'complete':
50
- succeedUfoExperience(id, {
51
- fileAttributes,
52
- ssrReliability,
53
- fileStateFlags
439
+ experience.success({
440
+ metadata: {
441
+ fileAttributes: sanitisedFileAttributes,
442
+ ssrReliability,
443
+ fileStateFlags,
444
+ ...getBasePayloadAttributes()
445
+ }
54
446
  });
55
447
  break;
56
448
  case 'failed-processing':
57
- failUfoExperience(id, {
58
- fileAttributes,
59
- failReason: 'failed-processing',
60
- ssrReliability,
61
- fileStateFlags
449
+ experience.failure({
450
+ metadata: {
451
+ fileAttributes: sanitisedFileAttributes,
452
+ ssrReliability,
453
+ fileStateFlags,
454
+ failReason: 'failed-processing',
455
+ ...getBasePayloadAttributes()
456
+ }
62
457
  });
63
458
  break;
64
459
  case 'error':
65
- failUfoExperience(id, {
66
- fileAttributes,
67
- ...extractErrorInfo(error),
68
- request: getRenderErrorRequestMetadata(error),
69
- ssrReliability,
70
- fileStateFlags
460
+ experience.failure({
461
+ metadata: {
462
+ fileAttributes: sanitisedFileAttributes,
463
+ ssrReliability,
464
+ fileStateFlags,
465
+ ...extractErrorInfo(error),
466
+ request: getRenderErrorRequestMetadata(error),
467
+ ...getBasePayloadAttributes()
468
+ }
71
469
  });
72
470
  break;
73
471
  }
74
472
  };
75
- const getBasePayloadAttributes = () => ({
76
- packageName,
77
- packageVersion,
78
- mediaEnvironment: getMediaEnvironment(),
79
- mediaRegion: getMediaRegion()
80
- });
81
- const succeedUfoExperience = (id, properties) => {
473
+ export const abortUfoExperience = (id, properties) => {
474
+ const metadata = {
475
+ ...getBasePayloadAttributes()
476
+ };
82
477
  if (properties !== null && properties !== void 0 && properties.fileAttributes) {
83
- properties.fileAttributes = sanitiseFileAttributes(properties.fileAttributes);
478
+ metadata.fileAttributes = sanitiseFileAttributes(properties.fileAttributes);
84
479
  }
85
- getExperience(id).success({
86
- metadata: {
87
- ...properties,
88
- ...getBasePayloadAttributes()
89
- }
90
- });
91
- };
92
- const failUfoExperience = (id, properties) => {
93
- if (properties !== null && properties !== void 0 && properties.fileAttributes) {
94
- properties.fileAttributes = sanitiseFileAttributes(properties.fileAttributes);
480
+ if (properties !== null && properties !== void 0 && properties.fileStateFlags) {
481
+ metadata.fileStateFlags = properties.fileStateFlags;
95
482
  }
96
- getExperience(id).failure({
97
- metadata: {
98
- ...properties,
99
- ...getBasePayloadAttributes()
100
- }
101
- });
102
- };
103
- export const abortUfoExperience = (id, properties) => {
104
- // UFO won't abort if it's already in a final state (succeeded, failed, aborted, etc)
105
- if (properties !== null && properties !== void 0 && properties.fileAttributes) {
106
- properties.fileAttributes = sanitiseFileAttributes(properties.fileAttributes);
483
+ if (properties !== null && properties !== void 0 && properties.ssrReliability) {
484
+ metadata.ssrReliability = properties.ssrReliability;
107
485
  }
108
486
  getExperience(id).abort({
109
- metadata: {
110
- ...properties,
111
- ...getBasePayloadAttributes()
112
- }
487
+ metadata
113
488
  });
114
- };
489
+ };
490
+
491
+ // Suppress unused type warnings - these are used for type documentation
@@ -13,7 +13,7 @@ import { useAnalyticsEvents } from '@atlaskit/analytics-next';
13
13
  import { fg } from '@atlaskit/platform-feature-flags';
14
14
  import UFOLabel from '@atlaskit/react-ufo/label';
15
15
  var packageName = "@atlaskit/media-card";
16
- var packageVersion = "79.11.3";
16
+ var packageVersion = "79.13.0";
17
17
  export var CardBase = function CardBase(_ref) {
18
18
  var identifier = _ref.identifier,
19
19
  otherProps = _objectWithoutProperties(_ref, _excluded);
@@ -11,7 +11,7 @@ import ReactDOM from 'react-dom';
11
11
  import { ImageLoadError } from '../errors';
12
12
  import { generateUniqueId } from '../utils/generateUniqueId';
13
13
  import { getMediaCardCursor } from '../utils/getMediaCardCursor';
14
- import { abortUfoExperience, completeUfoExperience, startUfoExperience, shouldPerformanceBeSampled } from '../utils/ufoExperiences';
14
+ import { shouldPerformanceBeSampled, useMediaCardUfoExperience } from '../utils/ufoExperiences';
15
15
  import { useCurrentValueRef } from '../utils/useCurrentValueRef';
16
16
  import { getDefaultCardDimensions } from '../utils/cardDimensions';
17
17
  import { usePrevious } from '../utils/usePrevious';
@@ -63,10 +63,17 @@ export var ExternalImageCard = function ExternalImageCard(_ref) {
63
63
  wasStatusUploading: false,
64
64
  wasStatusProcessing: false
65
65
  });
66
+ var shouldSendPerformanceEvent = useMemo(function () {
67
+ return shouldPerformanceBeSampled();
68
+ }, []);
69
+
70
+ // UFO experience hook - creates a unique experience instance per card
71
+ var ufoExperience = useMediaCardUfoExperience({
72
+ instanceId: internalOccurrenceKey,
73
+ enabled: shouldSendPerformanceEvent
74
+ });
66
75
  var startUfoExperienceRef = useCurrentValueRef(function () {
67
- if (shouldSendPerformanceEventRef.current) {
68
- startUfoExperience(internalOccurrenceKey);
69
- }
76
+ ufoExperience.start();
70
77
  });
71
78
  var _useState = useState('loading-preview'),
72
79
  _useState2 = _slicedToArray(_useState, 2),
@@ -110,7 +117,6 @@ export var ExternalImageCard = function ExternalImageCard(_ref) {
110
117
  _useState8 = _slicedToArray(_useState7, 2),
111
118
  mediaViewerSelectedItem = _useState8[0],
112
119
  setMediaViewerSelectedItem = _useState8[1];
113
- var shouldSendPerformanceEventRef = useRef(shouldPerformanceBeSampled());
114
120
 
115
121
  //----------------------------------------------------------------//
116
122
  //---------------------- Analytics ------------------------------//
@@ -126,17 +132,16 @@ export var ExternalImageCard = function ExternalImageCard(_ref) {
126
132
  }
127
133
  };
128
134
  createAnalyticsEvent && fireOperationalEvent(createAnalyticsEvent, status, fileAttributes, performanceAttributes, ssrReliability, error, traceContext, undefined);
129
- shouldSendPerformanceEventRef.current && completeUfoExperience(internalOccurrenceKey, status, fileAttributes, fileStateFlagsRef.current, ssrReliability, error);
135
+ // External images don't use SSR - no ssrPreviewInfo passed
136
+ ufoExperience.complete(status, fileAttributes, fileStateFlagsRef.current, ssrReliability, error, undefined);
130
137
  });
131
138
  var fireAbortedEventRef = useCurrentValueRef(function () {
132
139
  // UFO won't abort if it's already in a final state (succeeded, failed, aborted, etc)
133
- if (shouldSendPerformanceEventRef.current) {
134
- abortUfoExperience(internalOccurrenceKey, {
135
- fileAttributes: fileAttributes,
136
- fileStateFlags: fileStateFlagsRef === null || fileStateFlagsRef === void 0 ? void 0 : fileStateFlagsRef.current,
137
- ssrReliability: ssrReliability
138
- });
139
- }
140
+ ufoExperience.abort({
141
+ fileAttributes: fileAttributes,
142
+ fileStateFlags: fileStateFlagsRef === null || fileStateFlagsRef === void 0 ? void 0 : fileStateFlagsRef.current,
143
+ ssrReliability: ssrReliability
144
+ });
140
145
  });
141
146
 
142
147
  //----------------------------------------------------------------//