22 * License, v. 2.0. If a copy of the MPL was not distributed with this
33 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44import { oneLine } from 'common-tags' ;
5+ import {
6+ fetchProfile ,
7+ getProfileUrlForHash ,
8+ type ProfileOrZip ,
9+ deduceContentType ,
10+ extractJsonFromArrayBuffer ,
11+ } from 'firefox-profiler/utils/profile-fetch' ;
512import queryString from 'query-string' ;
613import type JSZip from 'jszip' ;
714import {
@@ -20,10 +27,8 @@ import {
2027} from 'firefox-profiler/profile-logic/symbolication' ;
2128import * as MozillaSymbolicationAPI from 'firefox-profiler/profile-logic/mozilla-symbolication-api' ;
2229import { mergeProfilesForDiffing } from 'firefox-profiler/profile-logic/merge-compare' ;
23- import { decompress , isGzip } from 'firefox-profiler/utils/gz' ;
2430import { expandUrl } from 'firefox-profiler/utils/shorten-url' ;
2531import { TemporaryError } from 'firefox-profiler/utils/errors' ;
26- import { isLocalURL } from 'firefox-profiler/utils/url' ;
2732import {
2833 getSelectedThreadIndexesOrNull ,
2934 getGlobalTrackOrder ,
@@ -67,7 +72,6 @@ import {
6772import { setDataSource } from './profile-view' ;
6873import { fatalError } from './errors' ;
6974import { batchLoadDataUrlIcons } from './icons' ;
70- import { GOOGLE_STORAGE_BUCKET } from 'firefox-profiler/app-logic/constants' ;
7175import {
7276 determineTimelineType ,
7377 hasUsefulSamples ,
@@ -547,7 +551,7 @@ async function _unpackGeckoProfileFromBrowser(
547551 // global. This happens especially with tests but could happen in the future
548552 // in Firefox too.
549553 if ( Object . prototype . toString . call ( profile ) === '[object ArrayBuffer]' ) {
550- return _extractJsonFromArrayBuffer ( profile as ArrayBuffer ) ;
554+ return extractJsonFromArrayBuffer ( profile as ArrayBuffer ) ;
551555 }
552556 return profile ;
553557}
@@ -557,9 +561,9 @@ function getSymbolStore(
557561 symbolServerUrl : string ,
558562 browserConnection : BrowserConnection | null
559563) : SymbolStore | null {
560- if ( ! window . indexedDB ) {
561- // We could be running in a test environment with no indexedDB support. Do not
562- // return a symbol store in this case.
564+ if ( typeof window === 'undefined' || ! window . indexedDB ) {
565+ // We could be running in a test environment or Node.js with no indexedDB support.
566+ // Do not return a symbol store in this case.
563567 return null ;
564568 }
565569
@@ -983,265 +987,6 @@ export function temporaryError(error: TemporaryError): Action {
983987 } ;
984988}
985989
986- function _wait ( delayMs : number ) : Promise < void > {
987- return new Promise ( ( resolve ) => setTimeout ( resolve , delayMs ) ) ;
988- }
989-
990- function _loadProbablyFailedDueToSafariLocalhostHTTPRestriction (
991- url : string ,
992- error : Error
993- ) : boolean {
994- if ( ! navigator . userAgent . match ( / S a f a r i \/ \d + \. \d + / ) ) {
995- return false ;
996- }
997- // Check if Safari considers this mixed content.
998- const parsedUrl = new URL ( url ) ;
999- return (
1000- error . name === 'TypeError' &&
1001- parsedUrl . protocol === 'http:' &&
1002- isLocalURL ( parsedUrl ) &&
1003- location . protocol === 'https:'
1004- ) ;
1005- }
1006-
1007- class SafariLocalhostHTTPLoadError extends Error {
1008- override name = 'SafariLocalhostHTTPLoadError' ;
1009- }
1010-
1011- type FetchProfileArgs = {
1012- url : string ;
1013- onTemporaryError : ( param : TemporaryError ) => void ;
1014- // Allow tests to capture the reported error, but normally use console.error.
1015- reportError ?: ( ...data : Array < any > ) => void ;
1016- } ;
1017-
1018- type ProfileOrZip =
1019- | { responseType : 'PROFILE' ; profile : unknown }
1020- | { responseType : 'ZIP' ; zip : JSZip } ;
1021-
1022- /**
1023- * Tries to fetch a profile on `url`. If the profile is not found,
1024- * `onTemporaryError` is called with an appropriate error, we wait 1 second, and
1025- * then tries again. If we still can't find the profile after 11 tries, the
1026- * returned promise is rejected with a fatal error.
1027- * If we can retrieve the profile properly, the returned promise is resolved
1028- * with the JSON.parsed profile.
1029- */
1030- export async function _fetchProfile (
1031- args : FetchProfileArgs
1032- ) : Promise < ProfileOrZip > {
1033- const MAX_WAIT_SECONDS = 10 ;
1034- let i = 0 ;
1035- const { url, onTemporaryError } = args ;
1036- // Allow tests to capture the reported error, but normally use console.error.
1037- const reportError = args . reportError || console . error ;
1038-
1039- while ( true ) {
1040- let response ;
1041- try {
1042- response = await fetch ( url ) ;
1043- } catch ( e ) {
1044- // Case 1: Exception.
1045- if ( _loadProbablyFailedDueToSafariLocalhostHTTPRestriction ( url , e ) ) {
1046- throw new SafariLocalhostHTTPLoadError ( ) ;
1047- }
1048- throw e ;
1049- }
1050-
1051- // Case 2: successful answer.
1052- if ( response . ok ) {
1053- return _extractProfileOrZipFromResponse ( url , response , reportError ) ;
1054- }
1055-
1056- // case 3: unrecoverable error.
1057- if ( response . status !== 403 ) {
1058- throw new Error ( oneLine `
1059- Could not fetch the profile on remote server.
1060- Response was: ${ response . status } ${ response . statusText } .
1061- ` ) ;
1062- }
1063-
1064- // case 4: 403 errors can be transient while a profile is uploaded.
1065-
1066- if ( i ++ === MAX_WAIT_SECONDS ) {
1067- // In the last iteration we don't send a temporary error because we'll
1068- // throw an error right after the while loop.
1069- break ;
1070- }
1071-
1072- onTemporaryError (
1073- new TemporaryError (
1074- 'Profile not found on remote server.' ,
1075- { count : i , total : MAX_WAIT_SECONDS + 1 } // 11 tries during 10 seconds
1076- )
1077- ) ;
1078-
1079- await _wait ( 1000 ) ;
1080- }
1081-
1082- throw new Error ( oneLine `
1083- Could not fetch the profile on remote server:
1084- still not found after ${ MAX_WAIT_SECONDS } seconds.
1085- ` ) ;
1086- }
1087-
1088- /**
1089- * Deduce the file type from a url and content type. Third parties can give us
1090- * arbitrary information, so make sure that we try out best to extract the proper
1091- * information about it.
1092- */
1093- function _deduceContentType (
1094- url : string ,
1095- contentType : string | null
1096- ) : 'application/json' | 'application/zip' | null {
1097- if ( contentType === 'application/zip' || contentType === 'application/json' ) {
1098- return contentType ;
1099- }
1100- if ( url . match ( / \. z i p $ / ) ) {
1101- return 'application/zip' ;
1102- }
1103- if ( url . match ( / \. j s o n / ) ) {
1104- return 'application/json' ;
1105- }
1106- return null ;
1107- }
1108-
1109- /**
1110- * This function guesses the correct content-type (even if one isn't sent) and then
1111- * attempts to use the proper method to extract the response.
1112- */
1113- async function _extractProfileOrZipFromResponse (
1114- url : string ,
1115- response : Response ,
1116- reportError : ( ...data : Array < any > ) => void
1117- ) : Promise < ProfileOrZip > {
1118- const contentType = _deduceContentType (
1119- url ,
1120- response . headers . get ( 'content-type' )
1121- ) ;
1122- switch ( contentType ) {
1123- case 'application/zip' :
1124- return {
1125- responseType : 'ZIP' ,
1126- zip : await _extractZipFromResponse ( response , reportError ) ,
1127- } ;
1128- case 'application/json' :
1129- case null :
1130- // The content type is null if it is unknown, or an unsupported type. Go ahead
1131- // and try to process it as a profile.
1132- return {
1133- responseType : 'PROFILE' ,
1134- profile : await _extractJsonFromResponse (
1135- response ,
1136- reportError ,
1137- contentType
1138- ) ,
1139- } ;
1140- default :
1141- throw assertExhaustiveCheck ( contentType ) ;
1142- }
1143- }
1144-
1145- /**
1146- * Attempt to load a zip file from a third party. This process can fail, so make sure
1147- * to handle and report the error if it does.
1148- */
1149- async function _extractZipFromResponse (
1150- response : Response ,
1151- reportError : ( ...data : Array < any > ) => void
1152- ) : Promise < JSZip > {
1153- const buffer = await response . arrayBuffer ( ) ;
1154- // Workaround for https://github.com/Stuk/jszip/issues/941
1155- // When running this code in tests, `buffer` doesn't inherits from _this_
1156- // realm's ArrayBuffer object, and this breaks JSZip which doesn't account for
1157- // this case. We workaround the issue by wrapping the buffer in an Uint8Array
1158- // that comes from this realm.
1159- const typedBuffer = new Uint8Array ( buffer ) ;
1160- try {
1161- const { default : JSZip } = await import ( 'jszip' ) ;
1162- const zip = await JSZip . loadAsync ( typedBuffer ) ;
1163- // Catch the error if unable to load the zip.
1164- return zip ;
1165- } catch ( error ) {
1166- const message = 'Unable to open the archive file.' ;
1167- reportError ( message ) ;
1168- reportError ( 'Error:' , error ) ;
1169- reportError ( 'Fetch response:' , response ) ;
1170- throw new Error (
1171- `${ message } The full error information has been printed out to the DevTool’s console.`
1172- ) ;
1173- }
1174- }
1175-
1176- /**
1177- * Parse JSON from an optionally gzipped array buffer.
1178- */
1179- async function _extractJsonFromArrayBuffer (
1180- arrayBuffer : ArrayBuffer
1181- ) : Promise < unknown > {
1182- let profileBytes = new Uint8Array ( arrayBuffer ) ;
1183- // Check for the gzip magic number in the header.
1184- if ( isGzip ( profileBytes ) ) {
1185- profileBytes = await decompress ( profileBytes ) ;
1186- }
1187-
1188- const textDecoder = new TextDecoder ( ) ;
1189- return JSON . parse ( textDecoder . decode ( profileBytes ) ) ;
1190- }
1191-
1192- /**
1193- * Don't trust third party responses, try and handle a variety of responses gracefully.
1194- */
1195- async function _extractJsonFromResponse (
1196- response : Response ,
1197- reportError : ( ...data : Array < any > ) => void ,
1198- fileType : 'application/json' | null
1199- ) : Promise < unknown > {
1200- let arrayBuffer : ArrayBuffer | null = null ;
1201- try {
1202- // await before returning so that we can catch JSON parse errors.
1203- arrayBuffer = await response . arrayBuffer ( ) ;
1204- return await _extractJsonFromArrayBuffer ( arrayBuffer ) ;
1205- } catch ( error ) {
1206- // Change the error message depending on the circumstance:
1207- let message ;
1208- if ( error && typeof error === 'object' && error . name === 'AbortError' ) {
1209- message = 'The network request to load the profile was aborted.' ;
1210- } else if ( fileType === 'application/json' ) {
1211- message = 'The profile’s JSON could not be decoded.' ;
1212- } else if ( fileType === null && arrayBuffer !== null ) {
1213- // If the content type is not specified, use a raw array buffer
1214- // to fallback to other supported profile formats.
1215- return arrayBuffer ;
1216- } else {
1217- message = oneLine `
1218- The profile could not be downloaded and decoded. This does not look like a supported file
1219- type.
1220- ` ;
1221- }
1222-
1223- // Provide helpful debugging information to the console.
1224- reportError ( message ) ;
1225- reportError ( 'JSON parsing error:' , error ) ;
1226- reportError ( 'Fetch response:' , response ) ;
1227-
1228- throw new Error (
1229- `${ message } The full error information has been printed out to the DevTool’s console.`
1230- ) ;
1231- }
1232- }
1233-
1234- export function getProfileUrlForHash ( hash : string ) : string {
1235- // See https://cloud.google.com/storage/docs/access-public-data
1236- // The URL is https://storage.googleapis.com/<BUCKET>/<FILEPATH>.
1237- // https://<BUCKET>.storage.googleapis.com/<FILEPATH> seems to also work but
1238- // is not documented nowadays.
1239-
1240- // By convention, "profile-store" is the name of our bucket, and the file path
1241- // is the hash we receive in the URL.
1242- return `https://storage.googleapis.com/${ GOOGLE_STORAGE_BUCKET } /${ hash } ` ;
1243- }
1244-
1245990export function retrieveProfileFromStore (
1246991 hash : string ,
1247992 initialLoad : boolean = false
@@ -1262,7 +1007,7 @@ export function retrieveProfileOrZipFromUrl(
12621007 dispatch ( waitingForProfileFromUrl ( profileUrl ) ) ;
12631008
12641009 try {
1265- const response : ProfileOrZip = await _fetchProfile ( {
1010+ const response : ProfileOrZip = await fetchProfile ( {
12661011 url : profileUrl ,
12671012 onTemporaryError : ( e : TemporaryError ) => {
12681013 dispatch ( temporaryError ( e ) ) ;
@@ -1293,7 +1038,7 @@ export function retrieveProfileOrZipFromUrl(
12931038 default :
12941039 throw assertExhaustiveCheck (
12951040 response as never ,
1296- 'Expected to receive an archive or profile from _fetchProfile .'
1041+ 'Expected to receive an archive or profile from fetchProfile .'
12971042 ) ;
12981043 }
12991044 } catch ( error ) {
@@ -1349,7 +1094,7 @@ export function retrieveProfileFromFile(
13491094 dispatch ( waitingForProfileFromFile ( ) ) ;
13501095
13511096 try {
1352- if ( _deduceContentType ( file . name , file . type ) === 'application/zip' ) {
1097+ if ( deduceContentType ( file . name , file . type ) === 'application/zip' ) {
13531098 // Open a zip file in the zip file viewer
13541099 const buffer = await fileReader ( file ) . asArrayBuffer ( ) ;
13551100 const { default : JSZip } = await import ( 'jszip' ) ;
@@ -1446,14 +1191,14 @@ export function retrieveProfilesToCompare(
14461191
14471192 const profileUrl = getProfileFetchUrl ( url ) ;
14481193
1449- const response : ProfileOrZip = await _fetchProfile ( {
1194+ const response : ProfileOrZip = await fetchProfile ( {
14501195 url : profileUrl ,
14511196 onTemporaryError : ( e : TemporaryError ) => {
14521197 dispatch ( temporaryError ( e ) ) ;
14531198 } ,
14541199 } ) ;
14551200 if ( response . responseType !== 'PROFILE' ) {
1456- throw new Error ( 'Expected to receive a profile from _fetchProfile ' ) ;
1201+ throw new Error ( 'Expected to receive a profile from fetchProfile ' ) ;
14571202 }
14581203
14591204 const upgradeInfo : ProfileUpgradeInfo = { } ;
0 commit comments