## Ticket 910 ## Changes - Change the "message" from a generic "tus-upload" to more specific ones like "tus: failed to resume upload". These are grouped as "Events" in sentry, so we can isolate and search for them easily. - Pass more info to Sentry (previously only available from Slack). It is still good to send to both, since some browsers block Sentry even without blocker extensions. - Reduce verbosity of Slack's ## Notes - Was unable to change the "unknown" problem mentioned in the ticket. The API does not accept `new Error('xxx')`, even though that's being mentioned by many in the forums. It might be due to the version of Sentry that we are using. - To search for tus issues, go to "Issues" and query `message:tus*`. Results are collapsed per event, so click on the item of interest, then click "Events" at the upper right to see all occurrences of the same problem.
154 lines
5.6 KiB
JavaScript
154 lines
5.6 KiB
JavaScript
// @flow
|
|
import * as tus from 'tus-js-client';
|
|
import NoopUrlStorage from 'tus-js-client/lib/noopUrlStorage';
|
|
import analytics from '../../ui/analytics';
|
|
import { X_LBRY_AUTH_TOKEN } from '../../ui/constants/token';
|
|
import { doUpdateUploadAdd, doUpdateUploadProgress, doUpdateUploadRemove } from '../../ui/redux/actions/publish';
|
|
import { LBRY_WEB_PUBLISH_API_V2 } from 'config';
|
|
|
|
const RESUMABLE_ENDPOINT = LBRY_WEB_PUBLISH_API_V2;
|
|
const RESUMABLE_ENDPOINT_METHOD = 'publish';
|
|
const UPLOAD_CHUNK_SIZE_BYTE = 10 * 1024 * 1024;
|
|
|
|
const STATUS_CONFLICT = 409;
|
|
const STATUS_LOCKED = 423;
|
|
|
|
/**
|
|
* Checks whether a given status is in the range of the expected category.
|
|
*
|
|
* @param status
|
|
* @param category
|
|
* @returns {boolean}
|
|
*/
|
|
function inStatusCategory(status, category) {
|
|
return status >= category && status < category + 100;
|
|
}
|
|
|
|
function getTusErrorType(errMsg: string) {
|
|
if (errMsg.startsWith('tus: failed to upload chunk at offset')) {
|
|
// This is the only message that contains dynamic value prior to the first comma.
|
|
return 'tus: failed to upload chunk at offset';
|
|
} else {
|
|
return errMsg.startsWith('tus:') ? errMsg.substring(0, errMsg.indexOf(',')) : errMsg;
|
|
}
|
|
}
|
|
|
|
export function makeResumableUploadRequest(
|
|
token: string,
|
|
params: FileUploadSdkParams,
|
|
file: File | string,
|
|
isPreview?: boolean
|
|
) {
|
|
return new Promise<any>((resolve, reject) => {
|
|
if (!RESUMABLE_ENDPOINT) {
|
|
reject(new Error('Publish: endpoint undefined'));
|
|
}
|
|
|
|
if (params.remote_url) {
|
|
reject(new Error('Publish: v2 does not support remote_url'));
|
|
}
|
|
|
|
const { uploadUrl, guid, ...sdkParams } = params;
|
|
|
|
const jsonPayload = JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
method: RESUMABLE_ENDPOINT_METHOD,
|
|
params: sdkParams,
|
|
id: new Date().getTime(),
|
|
});
|
|
|
|
const urlOptions = {};
|
|
if (params.uploadUrl) {
|
|
// Resuming from previous upload. TUS clears the resume fingerprint on any
|
|
// 4xx error, so we need to use the fixed URL mode instead.
|
|
urlOptions.uploadUrl = params.uploadUrl;
|
|
} else {
|
|
// New upload, so use `endpoint`.
|
|
urlOptions.endpoint = RESUMABLE_ENDPOINT;
|
|
}
|
|
|
|
const uploader = new tus.Upload(file, {
|
|
...urlOptions,
|
|
chunkSize: UPLOAD_CHUNK_SIZE_BYTE,
|
|
retryDelays: [5000, 10000, 30000],
|
|
parallelUploads: 1,
|
|
storeFingerprintForResuming: false,
|
|
urlStorage: new NoopUrlStorage(),
|
|
removeFingerprintOnSuccess: true,
|
|
headers: { [X_LBRY_AUTH_TOKEN]: token },
|
|
metadata: {
|
|
filename: file instanceof File ? file.name : file,
|
|
filetype: file instanceof File ? file.type : undefined,
|
|
},
|
|
onShouldRetry: (err, retryAttempt, options) => {
|
|
window.store.dispatch(doUpdateUploadProgress({ guid, status: 'retry' }));
|
|
const status = err.originalResponse ? err.originalResponse.getStatus() : 0;
|
|
return !inStatusCategory(status, 400) || status === STATUS_CONFLICT || status === STATUS_LOCKED;
|
|
},
|
|
onError: (err) => {
|
|
const status = err.originalResponse ? err.originalResponse.getStatus() : 0;
|
|
const errMsg = typeof err === 'string' ? err : err.message;
|
|
|
|
let customErr;
|
|
if (status === STATUS_LOCKED || errMsg === 'file currently locked') {
|
|
customErr = 'File is locked. Try resuming after waiting a few minutes';
|
|
}
|
|
|
|
window.store.dispatch(doUpdateUploadProgress({ guid, status: 'error' }));
|
|
analytics.sentryError(getTusErrorType(errMsg), { onError: err, tusUpload: uploader });
|
|
|
|
reject(
|
|
// $FlowFixMe - flow's constructor for Error is incorrect.
|
|
new Error(customErr || err, {
|
|
cause: {
|
|
...(uploader._fingerprint ? { fingerprint: uploader._fingerprint } : {}),
|
|
...(uploader._retryAttempt ? { retryAttempt: uploader._retryAttempt } : {}),
|
|
...(uploader._offsetBeforeRetry ? { offsetBeforeRetry: uploader._offsetBeforeRetry } : {}),
|
|
...(customErr ? { original: errMsg } : {}),
|
|
},
|
|
})
|
|
);
|
|
},
|
|
onProgress: (bytesUploaded, bytesTotal) => {
|
|
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
|
window.store.dispatch(doUpdateUploadProgress({ guid, progress: percentage }));
|
|
},
|
|
onSuccess: () => {
|
|
let retries = 1;
|
|
|
|
function makeNotifyRequest() {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open('POST', `${uploader.url}/notify`);
|
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
xhr.setRequestHeader('Tus-Resumable', '1.0.0');
|
|
xhr.setRequestHeader(X_LBRY_AUTH_TOKEN, token);
|
|
xhr.responseType = 'json';
|
|
xhr.onload = () => {
|
|
window.store.dispatch(doUpdateUploadRemove(guid));
|
|
resolve(xhr);
|
|
};
|
|
xhr.onerror = () => {
|
|
if (retries > 0 && xhr.status === 0) {
|
|
--retries;
|
|
analytics.error('notify: first attempt failed (status=0). Retrying after 10s...');
|
|
setTimeout(() => makeNotifyRequest(), 10000); // Auto-retry after 10s delay.
|
|
} else {
|
|
window.store.dispatch(doUpdateUploadProgress({ guid, status: 'error' }));
|
|
reject(new Error(`There was a problem in the processing. Please retry. (${xhr.status})`));
|
|
}
|
|
};
|
|
xhr.onabort = () => {
|
|
window.store.dispatch(doUpdateUploadRemove(guid));
|
|
};
|
|
|
|
xhr.send(jsonPayload);
|
|
}
|
|
|
|
setTimeout(() => makeNotifyRequest(), 15000);
|
|
},
|
|
});
|
|
|
|
window.store.dispatch(doUpdateUploadAdd(file, params, uploader));
|
|
uploader.start();
|
|
});
|
|
}
|