-
Notifications
You must be signed in to change notification settings - Fork 9.4k
/
github-api.js
165 lines (148 loc) · 5.91 KB
/
github-api.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
/**
* @license Copyright 2016 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';
/* global logger */
/** @typedef {{etag: ?string, content: LH.Result}} CachableGist */
import idbKeyval from 'idb-keyval';
import {FirebaseAuth} from './firebase-auth.js';
// eslint-disable-next-line max-len
import {getLhrFilenamePrefix, getFlowResultFilenamePrefix} from '../../../report/generator/file-namer.js';
/**
* Wrapper around the GitHub API for reading/writing gists.
*/
export class GithubApi {
constructor() {
this._auth = new FirebaseAuth();
this._saving = false;
}
static get LH_JSON_EXT() {
return '.lighthouse.report.json';
}
getFirebaseAuth() {
return this._auth;
}
/**
* Creates a gist under the users account.
* @param {LH.Result|LH.FlowResult} jsonFile The gist file body.
* @return {Promise<string>} id of the created gist.
*/
async createGist(jsonFile) {
if (this._saving) {
throw new Error('Save already in progress');
}
logger.log('Saving report to GitHub...', false);
this._saving = true;
try {
const accessToken = await this._auth.getAccessToken();
let filename;
if ('steps' in jsonFile) {
filename = getFlowResultFilenamePrefix(jsonFile);
} else {
filename = getLhrFilenamePrefix({
finalUrl: jsonFile.finalUrl,
fetchTime: jsonFile.fetchTime,
});
}
const body = {
description: 'Lighthouse json report',
public: false,
files: {
[`${filename}${GithubApi.LH_JSON_EXT}`]: {
content: JSON.stringify(jsonFile),
},
},
};
const request = new Request('https://api.github.com/gists', {
method: 'POST',
headers: new Headers({Authorization: `token ${accessToken}`}),
// Stringify twice so quotes are escaped for POST request to succeed.
body: JSON.stringify(body),
});
const response = await fetch(request);
const json = await response.json();
if (json.id) {
logger.log('Saved!');
return json.id;
} else {
throw new Error('Error: ' + JSON.stringify(json));
}
} finally {
this._saving = false;
}
}
/**
* Fetches a Lighthouse report from a gist.
* @param {string} id The id of a gist.
* @return {Promise<LH.Result>}
*/
getGistFileContentAsJson(id) {
logger.log('Fetching report from GitHub...', false);
return this._auth.getAccessTokenIfLoggedIn().then(accessToken => {
const headers = new Headers();
// If there's an authenticated token, include an Authorization header to
// have higher rate limits with the GitHub API. Otherwise, rely on ETags.
if (accessToken) {
headers.set('Authorization', `token ${accessToken}`);
}
return idbKeyval.get(id).then(/** @param {?CachableGist} cachedGist */ (cachedGist) => {
if (cachedGist && cachedGist.etag) {
headers.set('If-None-Match', cachedGist.etag);
}
// Always make the request to see if there's newer content.
return fetch(`https://api.github.com/gists/${id}`, {headers}).then(resp => {
const remaining = Number(resp.headers.get('X-RateLimit-Remaining'));
const limit = Number(resp.headers.get('X-RateLimit-Limit'));
if (remaining < 10) {
logger.warn('Approaching GitHub\'s rate limit. ' +
`${limit - remaining}/${limit} requests used. Consider signing ` +
'in to increase this limit.');
}
if (!resp.ok) {
// Should only be 304 if cachedGist exists and etag was sent, but double check.
if (resp.status === 304 && cachedGist) {
return Promise.resolve(cachedGist);
} else if (resp.status === 404) {
// Delete the entry from IDB if it no longer exists on the server.
idbKeyval.delete(id); // Note: async.
}
throw new Error(`${resp.status} fetching gist`);
}
const etag = resp.headers.get('ETag');
return resp.json().then(json => {
const gistFiles = Object.keys(json.files);
// Attempt to use first file in gist with report extension.
let filename = gistFiles.find(filename => filename.endsWith(GithubApi.LH_JSON_EXT));
// Otherwise, fall back to first json file in gist
if (!filename) {
filename = gistFiles.find(filename => filename.endsWith('.json'));
}
if (!filename) {
throw new Error(
`Failed to find a Lighthouse report (*${GithubApi.LH_JSON_EXT}) in gist ${id}`
);
}
const f = json.files[filename];
if (f.truncated) {
return fetch(f.raw_url)
.then(resp => resp.json())
.then(content => ({etag, content}));
}
const lhr = /** @type {LH.Result} */ (JSON.parse(f.content));
return {etag, content: lhr};
});
});
});
}).then(response => {
// Cache the contents to speed up future lookups, even if an invalid
// report. Future requests for the id will either still be invalid or will
// not return a 304 and so will be overwritten.
return idbKeyval.set(id, response).then(_ => {
logger.hide();
return response.content;
});
});
}
}