#!/usr/bin/env /usr/local/bin/node const https = require('https'); /* EDIT HERE */ const AUTH_TOKEN = ''; const ORGANIZATION = ''; const PROJECT = ''; const ISSUE_COUNT = 5; /* DON'T EDIT BELOW */ // Sentry // v1.0 // Gil Barbara // gilbarbara // List recent issues. // node // https://github.com/gilbarbara/bitbar-plugins const BASE_URL = 'sentry.io'; const PROJECT_URL = `https://sentry.io/${ORGANIZATION}/${PROJECT}`; const TITLE = [`${ORGANIZATION}/${PROJECT}`, '@', 'Sentry'].join(' '); const ICON = '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wo2SxrHjccdv/eD1Q3sTq+lQM6O5+AQ4ITnx4sf4H++eDDzZeYp9qukn/Z/2ttBailqh1tzWibakNml7THvf6YDTnR3OHS0/m/989Iz2mZqzymdLz5HOFZybOZ93fvJCxoXxi4kXhzpXdD66tOTSna6wrt7LgZevXvG5cqnbvfv8VZerZ645XTt9nX297Yb9jdYeu56WX+x+aem172296XCz/ZbjrY6+BX3n+l37L972un3ljv+dGwOLBvruLr57/17cPel93v3RB6kPXj/Mejj9aP1j7OOiJwpPKp6qP6391fjXZqm99Oyg12DPs4hnj4a4Qy//lfmvT8MFz6nPK0a0RupHrUfPjPmM3Xqx9MXwy4yX0+OFvyn+tveV0auffnf7vWdiycTwa9HrmT9K3qi+OfrW9m3nZOjk03dp76anit6rvj/2gf2h+2P0x5Hp7E/4T5WfjT93fAn88ngmbWbm3/eE8/sKZW5kc3RyZWFtCmVuZG9iago5IDAgb2JqCjI2MTIKZW5kb2JqCjcgMCBvYmoKWyAvSUNDQmFzZWQgOCAwIFIgXQplbmRvYmoKMyAwIG9iago8PCAvVHlwZSAvUGFnZXMgL01lZGlhQm94IFswIDAgMTYgMTVdIC9Db3VudCAxIC9LaWRzIFsgMiAwIFIgXSA+PgplbmRvYmoKMTAgMCBvYmoKPDwgL1R5cGUgL0NhdGFsb2cgL1BhZ2VzIDMgMCBSID4+CmVuZG9iagoxMSAwIG9iagooTWFjIE9TIFggMTAuMTMuNiBRdWFydHogUERGQ29udGV4dCkKZW5kb2JqCjEyIDAgb2JqCihEOjIwMTgwODExMTYyMTIxWjAwJzAwJykKZW5kb2JqCjEgMCBvYmoKPDwgL1Byb2R1Y2VyIDExIDAgUiAvQ3JlYXRpb25EYXRlIDEyIDAgUiAvTW9kRGF0ZSAxMiAwIFIgPj4KZW5kb2JqCnhyZWYKMCAxMwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDQyNTggMDAwMDAgbiAKMDAwMDAwMTExNiAwMDAwMCBuIAowMDAwMDA0MDMyIDAwMDAwIG4gCjAwMDAwMDAwMjIgMDAwMDAgbiAKMDAwMDAwMTA5NiAwMDAwMCBuIAowMDAwMDAxMTk2IDAwMDAwIG4gCjAwMDAwMDM5OTcgMDAwMDAgbiAKMDAwMDAwMTI2NCAwMDAwMCBuIAowMDAwMDAzOTc3IDAwMDAwIG4gCjAwMDAwMDQxMTMgMDAwMDAgbiAKMDAwMDAwNDE2MyAwMDAwMCBuIAowMDAwMDA0MjE2IDAwMDAwIG4gCnRyYWlsZXIKPDwgL1NpemUgMTMgL1Jvb3QgMTAgMCBSIC9JbmZvIDEgMCBSIC9JRCBbIDw5YTkwNjRhZjYwNGZjMDE4MmFhZjg4OGZlNTI4NGUwMD4KPDlhOTA2NGFmNjA0ZmMwMTgyYWFmODg4ZmU1Mjg0ZTAwPiBdID4+CnN0YXJ0eHJlZgo0MzMzCiUlRU9GCg=='; const RELOAD_ICON = 'iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAmElEQVR4AY3SJdYCYRhA4V+BRGY5rAF3ZwvkiSyANLugYgUtOJsg4s7LxV3uOU8a+fTrom9EUMEAfZQRwDEFuwxIQ9CEetCBIIk4BF/fSKMPL26LYgHZoQgEXjxKgQCgCpqfvQwaQMVtOhhv3X7wURU08apvxGB5v2giFwTmT7bVigly+H11cAnUIChAD3p+NbrIwotv7NoAffg2NR6lsPIAAAAASUVORK5CYII='; function request(options = {}) { const OPTIONS = { hostname: BASE_URL, path: `/api/0/projects/${ORGANIZATION}/${PROJECT}/issues/?query=is%3Aunresolved&limit=${ISSUE_COUNT}&sort=date&statsPeriod=24h`, port: 443, method: options.method || 'GET', headers: { Authorization: `Bearer ${AUTH_TOKEN}`, }, }; return new Promise((resolve, reject) => { const req = https.request(OPTIONS, (response) => { const { headers, statusCode } = response; if (statusCode < 200 || statusCode > 299) { reject(new Error(`Request failed - status code: ${response.statusCode}`)); } const isJSON = headers['content-type'].includes('application/json'); // temporary data holder const body = []; // on every content chunk, push it to the data array response.on('data', chunk => body.push(chunk)); // we are done, resolve promise with those joined chunks response.on('end', () => { const content = body.join(''); resolve(isJSON ? JSON.parse(content) : content); }); }); // handle connection errors of the request req.on('error', err => reject(err)); req.end(); }); } function statusColor(issue) { const { assignedTo, status } = issue; const isAssigned = assignedTo !== null; if (status === 'resolved' || status === 'muted') { return 'green'; } if (status === 'unresolved' && isAssigned) { return 'orange'; } return 'red'; } function timeSince(dateString) { const date = new Date(dateString); const seconds = Math.floor((new Date() - date) / 1000); let intervalType; let interval = Math.floor(seconds / 31536000); if (interval >= 1) { intervalType = 'year'; } else { interval = Math.floor(seconds / 2592000); if (interval >= 1) { intervalType = 'month'; } else { interval = Math.floor(seconds / 86400); if (interval >= 1) { intervalType = 'day'; } else { interval = Math.floor(seconds / 3600); if (interval >= 1) { intervalType = 'hour'; } else { interval = Math.floor(seconds / 60); if (interval >= 1) { intervalType = 'minute'; } else { interval = seconds; intervalType = 'second'; } } } } } if (interval > 1 || interval === 0) { intervalType += 's'; } return `${interval} ${intervalType}`; } function trimString(str, n = 72) { return (str.length > n) ? `${str.substr(0, n - 1)}…` : str; } function formatTitle(issue) { return `${trimString(issue.title)} | href=${issue.permalink} color=${statusColor(issue)}`; } function formatCount(count) { const str = (count === 1) ? 'occurrence' : 'occurrences'; return `${count} ${str} | size=10`; } function formatTimes(issue) { const lastSeen = `${timeSince(issue.lastSeen)} ago`; const firstSeen = `${timeSince(issue.firstSeen)} old`; return `${lastSeen} - ${firstSeen} | size=12`; } function formatIssue(issue) { return [ formatTitle(issue), formatTimes(issue), formatCount(issue.count), ].join('\n'); } function handleResponse(body) { const content = body.map(formatIssue).join('\n---\n'); const output = [ `|image=${ICON}`, `${TITLE} | href=${PROJECT_URL}`, content, `RELOAD | image=${RELOAD_ICON} refresh=true`, ]; console.log(output.join('\n---\n')); } request() .then(handleResponse) .catch(err => console.log(err.toString()));