Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 2 additions & 15 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,36 +32,23 @@ jobs:
- name: Run Tests
run: npm test

- name: Prepare Wrangler Config
run: |
SENTRY_DSN=$(printf '%s\n' "${{ secrets.SENTRY_DSN }}" | sed -e 's/[\/&]/\\&/g')
sed -i "s/SENTRY_DSN = \"\"/SENTRY_DSN = \"$SENTRY_DSN\"/g" wrangler.toml

SENTRY_RELEASE=$(printf '%s\n' "${{ github.sha }}" | sed -e 's/[\/&]/\\&/g')
sed -i "s/SENTRY_RELEASE = \"\"/SENTRY_RELEASE = \"$SENTRY_RELEASE\"/g" wrangler.toml

- name: Deploy to ${{ github.ref_name }}
run: npx wrangler deploy --env ${{ github.ref_name }} --outdir dist-worker --minify
run: npx wrangler deploy --env ${{ github.ref_name }} --outdir dist-worker --minify --var SENTRY_DSN:${{ secrets.SENTRY_DSN }} --var SENTRY_RELEASE:${{ github.sha }}
env:
CLOUDFLARE_API_TOKEN: ${{ github.ref_name == 'production' && secrets.CLOUDFLARE_API_TOKEN_PRODUCTION || secrets.CLOUDFLARE_API_TOKEN_STAGING }}
CLOUDFLARE_ACCOUNT_ID: ${{ github.ref_name == 'production' && secrets.CLOUDFLARE_ACCOUNT_ID_PRODUCTION || secrets.CLOUDFLARE_ACCOUNT_ID_STAGING }}

- name: Prepare Sentry Release
run: |
mv dist-worker/index.js dist-worker/worker.js
mv dist-worker/index.js.map dist-worker/worker.js.map

- name: Create Sentry Release
uses: getsentry/action-release@dab6548b3c03c4717878099e43782cf5be654289 # v3.5.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: cdnjs
SENTRY_PROJECT: api-worker
with:
url_prefix: /
ignore_missing: true
ignore_empty: true
inject: false
release: ${{ github.sha }}
environment: ${{ github.ref_name }}
sourcemaps: dist-worker
strip_common_prefix: true
273 changes: 59 additions & 214 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,12 @@
"node": ">=24.11.0"
},
"dependencies": {
"@sentry/integrations": "^7.114.0",
"@sentry/cloudflare": "^10.38.0",
"algoliasearch": "^4.22.0",
"hono": "^4.11.7",
"is-deflate": "^1.0.0",
"is-gzip": "^2.0.0",
"pako": "^2.1.0",
"toucan-js": "^3.3.1"
"pako": "^2.1.0"
},
"devDependencies": {
"@babel/core": "^7.29.0",
Expand Down
59 changes: 6 additions & 53 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { RewriteFrames } from '@sentry/integrations';
import { env } from 'cloudflare:workers';
import * as Sentry from '@sentry/cloudflare';
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { Toucan, RequestData } from 'toucan-js';

import errorRoutes from './routes/errors.js';
import indexRoutes from './routes/index.js';
Expand All @@ -18,55 +16,6 @@ const app = new Hono();
app.use('*', logger());
app.use('*', cors(corsOptions));

// Inject Sentry
if (env.SENTRY_DSN) {
app.use('*', async (ctx, next) => {
// Create the Sentry instance
ctx.sentry = new Toucan({
dsn: env.SENTRY_DSN,
context: {
waitUntil: ctx.executionCtx.waitUntil.bind(ctx.executionCtx),
request: ctx.req,
},
integrations: [
new RequestData({
allowedHeaders: [ 'user-agent', 'cf-ray' ],
allowedSearchParams: /(.*)/,
}),
new RewriteFrames({
/**
* @template {{ filename: string }} T
*
* Rewrite error stack frames to fix the source file path.
*
* @param {T} frame Stack frame to fix.
* @return {T}
*/
iteratee: frame => {
// Root should be `/`
frame.filename = frame.filename.replace(/^(async )?worker\.js/, '/worker.js');
return frame;
},
}),
],
release: env.SENTRY_RELEASE || undefined,
environment: env.SENTRY_ENVIRONMENT || undefined,
});

// Track the colo we're in
const colo = ctx.req.raw.cf?.colo || 'UNKNOWN';
ctx.sentry.setTag('colo', colo);

// Track the connecting user
const ipAddress = ctx.req.header('cf-connecting-ip') || ctx.req.header('x-forwarded-for') || undefined;
const userAgent = ctx.req.header('user-agent') || undefined;
ctx.sentry.setUser({ ip: ipAddress, userAgent, colo });

// Continue
await next();
});
}

// Load the routes
indexRoutes(app);
statsRoutes(app);
Expand All @@ -76,4 +25,8 @@ librariesRoutes(app);
errorRoutes(app);

// Let's go!
export default app;
export default Sentry.withSentry(env => ({
dsn: env.SENTRY_DSN,
release: env.SENTRY_RELEASE,
environment: env.SENTRY_ENVIRONMENT,
}), app);
23 changes: 22 additions & 1 deletion src/routes/errors.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as Sentry from '@sentry/cloudflare';

import cache from '../utils/cache.js';
import notFound from '../utils/notFound.js';
import respond from '../utils/respond.js';
Expand All @@ -8,6 +10,25 @@ import respond from '../utils/respond.js';
* @param {import('hono').Hono} app App instance.
*/
export default app => {
// Pass request context to Sentry
app.use('*', async (ctx, next) => {
Sentry.setTags({
requestId: crypto.randomUUID(),
userAgent: ctx.req.header('user-agent'),
ray: ctx.req.header('cf-ray'),
country: ctx.req.raw.cf?.country,
colo: ctx.req.raw.cf?.colo,
});

Sentry.setUser({
ip: ctx.req.header('cf-connecting-ip') || ctx.req.header('x-forwarded-for'),
userAgent: ctx.req.header('user-agent'),
colo: ctx.req.raw.cf?.colo,
});

await next();
});

// Handle 404s
app.notFound(ctx => notFound(ctx, 'Endpoint'));

Expand All @@ -21,7 +42,7 @@ export default app => {
app.onError((err, ctx) => {
// Log the error
console.error(err.stack);
const sentry = ctx.sentry?.captureException(err);
const sentry = Sentry.captureException(err);

// Never cache this
cache(ctx, -1);
Expand Down
5 changes: 3 additions & 2 deletions src/routes/libraries.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Sentry from '@sentry/cloudflare';
import { env } from 'cloudflare:workers';

import algolia from '../utils/algolia.js';
Expand Down Expand Up @@ -92,9 +93,9 @@ const handleGetLibraries = async ctx => {
if (hit?.name) return true;
console.warn('Found bad entry in Algolia data');
console.info(hit);
ctx.sentry?.withScope(scope => {
Sentry.withScope(scope => {
scope.setExtra('hit', hit);
ctx.sentry.captureException(new Error('Bad entry in Algolia data'));
Sentry.captureException(new Error('Bad entry in Algolia data'));
});
return false;
}).map(hit => filter(
Expand Down
9 changes: 4 additions & 5 deletions src/routes/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const whitelisted = file => extensions.includes(file.split('.').slice(-1)[0]);
*/
const handleGetLibraryVersion = async ctx => {
// Get the library
const lib = await library(ctx.req.param('library'), ctx.sentry).catch(err => {
const lib = await library(ctx.req.param('library')).catch(err => {
if (err.status === 404) return;
throw err;
});
Expand Down Expand Up @@ -60,7 +60,7 @@ const handleGetLibraryVersion = async ctx => {
if ('sri' in response) {
// Get SRI for version
const latestSriData = await libraryVersionSri(lib.name, ctx.req.param('version')).catch(() => {});
response.sri = sriForVersion(lib.name, ctx.req.param('version'), version, latestSriData, ctx.sentry);
response.sri = sriForVersion(lib.name, ctx.req.param('version'), version, latestSriData);
}

// Set a 355 day (same as CDN) life on this response
Expand All @@ -79,7 +79,7 @@ const handleGetLibraryVersion = async ctx => {
*/
const handleGetLibrary = async ctx => {
// Get the library
const lib = await library(ctx.req.param('library'), ctx.sentry).catch(err => {
const lib = await library(ctx.req.param('library')).catch(err => {
if (err.status === 404) return;
throw err;
});
Expand Down Expand Up @@ -123,7 +123,7 @@ const handleGetLibrary = async ctx => {
version: lib.version,
files: assets.filter(whitelisted),
rawFiles: assets,
sri: sriForVersion(lib.name, lib.version, assets, sriData, ctx.sentry),
sri: sriForVersion(lib.name, lib.version, assets, sriData),
} ];
}
}
Expand All @@ -150,7 +150,6 @@ const handleGetLibrary = async ctx => {
lib.version,
[ lib.filename ],
latestSriData,
ctx.sentry,
)[lib.filename] || null;
}
}
Expand Down
13 changes: 6 additions & 7 deletions src/utils/kvMetadata.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Sentry from '@sentry/cloudflare';
import { env } from 'cloudflare:workers';

import fetchJson from './fetchJson.js';
Expand All @@ -18,10 +19,9 @@ export const libraries = () => fetchJson(`${kvBase}/packages`);
*
* @param {string} library Requested library name.
* @param {T} data Returned library data to validate.
* @param {import('toucan-js')} [sentry] Sentry instance for missing version reporting.
* @return {T & { assets: [] }}
*/
const kvLibraryValidate = (library, data, sentry = undefined) => {
const kvLibraryValidate = (library, data) => {
// Assets might not exist if there are none, but we should make it an empty array by default
data.assets = data.assets || [];

Expand All @@ -34,9 +34,9 @@ const kvLibraryValidate = (library, data, sentry = undefined) => {
// Breaking issues
if (!data.version) {
console.error('Version missing', data.name, data);
sentry?.withScope(scope => {
Sentry.withScope(scope => {
scope.setExtra('data', data);
sentry.captureException(new Error('Version missing in package data'));
Sentry.captureException(new Error('Version missing in package data'));
});
throw new Error('Version missing in package data');
}
Expand All @@ -48,11 +48,10 @@ const kvLibraryValidate = (library, data, sentry = undefined) => {
* Get the metadata for a library.
*
* @param {string} name Name of the library to fetch.
* @param {import('toucan-js')} [sentry] Sentry instance for data validation reporting.
* @return {Promise<Object>}
*/
export const library = (name, sentry = undefined) => fetchJson(`${kvBase}/packages/${encodeURIComponent(name)}`)
.then(data => kvLibraryValidate(name, data, sentry));
export const library = name => fetchJson(`${kvBase}/packages/${encodeURIComponent(name)}`)
.then(data => kvLibraryValidate(name, data));

/**
* Get the versions for a library.
Expand Down
1 change: 1 addition & 0 deletions src/utils/spec/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @property {function(string): string|undefined} getHeader Method to access a header (alias to headers.get).
* @property {Request} request Request that was sent to fetch the response.
* @property {string} text Text content from the response.
* @property {ReadableStream|*} body Body content from the response (parsed JSON if response declared as JSON).

Check warning on line 12 in src/utils/spec/request.js

View workflow job for this annotation

GitHub Actions / test

Prefer a more specific type to `*`

Check warning on line 12 in src/utils/spec/request.js

View workflow job for this annotation

GitHub Actions / test

Prefer a more specific type to `*`

Check warning on line 12 in src/utils/spec/request.js

View workflow job for this annotation

GitHub Actions / test

Prefer a more specific type to `*`

Check warning on line 12 in src/utils/spec/request.js

View workflow job for this annotation

GitHub Actions / deploy

Prefer a more specific type to `*`
*/

/**
Expand All @@ -26,6 +26,7 @@
if (!mf) {
mf = new Miniflare({
modules: [ { type: 'ESModule', path: fileURLToPath(new URL('../../../dist-worker/index.js', import.meta.url)) } ],
compatibilityFlags: [ 'nodejs_als' ],
kvNamespaces: [ 'CACHE' ],
bindings: {
DISABLE_CACHING: false,
Expand Down
9 changes: 5 additions & 4 deletions src/utils/sriForVersion.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// import * as Sentry from '@sentry/cloudflare';

/**
* Create a map of file names to SRI hashes, based on library files and SRI data.
*
* @param {string} library Name of the library.
* @param {string} version Version of the library.
* @param {string[]} files Names of the files for this version of the library.
* @param {Object<string, string>} sriData SRI data for the libary version.
* @param {import('toucan-js')} [_sentry] Sentry instance for missing SRI reporting.
* @return {Object<string, string>}
*/
export default (library, version, files, sriData, _sentry = undefined) => {
export default (library, version, files, sriData) => {
// Build the SRI object
const sri = {};
for (const file of files) {
Expand All @@ -23,12 +24,12 @@ export default (library, version, files, sriData, _sentry = undefined) => {
// If we don't have an SRI entry, but expect one, error!
if (file.endsWith('.js') || file.endsWith('.css')) {
console.warn('Missing SRI entry for', fullFile);
// sentry?.withScope(scope => {
// Sentry.withScope(scope => {
// scope.setTag('library', library);
// scope.setTag('library.version', version);
// scope.setTag('library.file', file);
// scope.setTag('library.file.full', fullFile);
// sentry.captureException(new Error('Missing SRI entry'));
// Sentry.captureException(new Error('Missing SRI entry'));
// });
}
}
Expand Down
1 change: 1 addition & 0 deletions wrangler.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name = "cdnjs-api-worker"
main = "src/index.js"
compatibility_date = "2022-05-20"
compatibility_flags = [ "nodejs_als" ]
kv_namespaces = [
{ binding = "CACHE", id = "845ae1599dcf4d75950b61201a951b73", preview_id = "845ae1599dcf4d75950b61201a951b73" }
]
Expand Down
Loading