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