@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.
- package/CHANGELOG.md +22 -0
- package/dist/cjs/card/card.js +1 -1
- package/dist/cjs/card/externalImageCard.js +17 -12
- package/dist/cjs/card/fileCard.js +34 -12
- package/dist/cjs/card/media-card-analytics-error-boundary.js +1 -1
- package/dist/cjs/card/ui/tickBox/tickBoxWrapper-compiled.compiled.css +0 -3
- package/dist/cjs/card/ui/tickBox/tickBoxWrapper-compiled.js +1 -3
- package/dist/cjs/card/ui/titleBox/failedTitleBox.js +0 -2
- package/dist/cjs/card/ui/titleBox/titleBoxComponents-compiled.js +1 -3
- package/dist/cjs/inline/loader.js +1 -1
- package/dist/cjs/inline/mediaInlineAnalyticsErrorBoundary.js +0 -2
- package/dist/cjs/utils/globalScope/globalScope.js +2 -1
- package/dist/cjs/utils/mediaPerformanceObserver/durationMetrics.js +1 -0
- package/dist/cjs/utils/ufoExperiences.js +454 -68
- package/dist/es2019/card/card.js +1 -1
- package/dist/es2019/card/externalImageCard.js +16 -13
- package/dist/es2019/card/fileCard.js +33 -13
- package/dist/es2019/card/media-card-analytics-error-boundary.js +1 -1
- package/dist/es2019/card/ui/tickBox/tickBoxWrapper-compiled.compiled.css +0 -3
- package/dist/es2019/card/ui/tickBox/tickBoxWrapper-compiled.js +1 -3
- package/dist/es2019/card/ui/titleBox/failedTitleBox.js +0 -2
- package/dist/es2019/card/ui/titleBox/titleBoxComponents-compiled.js +1 -3
- package/dist/es2019/inline/loader.js +1 -1
- package/dist/es2019/inline/mediaInlineAnalyticsErrorBoundary.js +0 -2
- package/dist/es2019/utils/globalScope/globalScope.js +1 -0
- package/dist/es2019/utils/mediaPerformanceObserver/durationMetrics.js +1 -0
- package/dist/es2019/utils/ufoExperiences.js +449 -72
- package/dist/esm/card/card.js +1 -1
- package/dist/esm/card/externalImageCard.js +18 -13
- package/dist/esm/card/fileCard.js +35 -13
- package/dist/esm/card/media-card-analytics-error-boundary.js +1 -1
- package/dist/esm/card/ui/tickBox/tickBoxWrapper-compiled.compiled.css +0 -3
- package/dist/esm/card/ui/tickBox/tickBoxWrapper-compiled.js +1 -3
- package/dist/esm/card/ui/titleBox/failedTitleBox.js +0 -2
- package/dist/esm/card/ui/titleBox/titleBoxComponents-compiled.js +1 -3
- package/dist/esm/inline/loader.js +1 -1
- package/dist/esm/inline/mediaInlineAnalyticsErrorBoundary.js +0 -2
- package/dist/esm/utils/globalScope/globalScope.js +1 -0
- package/dist/esm/utils/mediaPerformanceObserver/durationMetrics.js +1 -0
- package/dist/esm/utils/ufoExperiences.js +454 -68
- package/dist/types/utils/globalScope/globalScope.d.ts +2 -0
- package/dist/types/utils/mediaPerformanceObserver/durationMetrics.d.ts +1 -0
- package/dist/types/utils/ufoExperiences.d.ts +88 -5
- package/dist/types-ts4.5/utils/globalScope/globalScope.d.ts +2 -0
- package/dist/types-ts4.5/utils/mediaPerformanceObserver/durationMetrics.d.ts +1 -0
- package/dist/types-ts4.5/utils/ufoExperiences.d.ts +88 -5
- package/package.json +9 -9
|
@@ -1,12 +1,386 @@
|
|
|
1
|
-
import {
|
|
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
|
+
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
|
|
26
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
478
|
+
metadata.fileAttributes = sanitiseFileAttributes(properties.fileAttributes);
|
|
84
479
|
}
|
|
85
|
-
|
|
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
|
-
|
|
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
|
package/dist/esm/card/card.js
CHANGED
|
@@ -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.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
//----------------------------------------------------------------//
|