Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
467 lines
14 KiB
Text
467 lines
14 KiB
Text
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
|
const breadcrumbs = require('../breadcrumbs.js');
|
|
const debugBuild = require('../debug-build.js');
|
|
const exports$1 = require('../exports.js');
|
|
const integration = require('../integration.js');
|
|
const semanticAttributes = require('../semanticAttributes.js');
|
|
const debugLogger = require('../utils/debug-logger.js');
|
|
const is = require('../utils/is.js');
|
|
const spanstatus = require('../tracing/spanstatus.js');
|
|
const trace = require('../tracing/trace.js');
|
|
|
|
// Based on Kamil Ogórek's work on:
|
|
// https://github.com/supabase-community/sentry-integration-js
|
|
|
|
|
|
const AUTH_OPERATIONS_TO_INSTRUMENT = [
|
|
'reauthenticate',
|
|
'signInAnonymously',
|
|
'signInWithOAuth',
|
|
'signInWithIdToken',
|
|
'signInWithOtp',
|
|
'signInWithPassword',
|
|
'signInWithSSO',
|
|
'signOut',
|
|
'signUp',
|
|
'verifyOtp',
|
|
];
|
|
|
|
const AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT = [
|
|
'createUser',
|
|
'deleteUser',
|
|
'listUsers',
|
|
'getUserById',
|
|
'updateUserById',
|
|
'inviteUserByEmail',
|
|
];
|
|
|
|
const FILTER_MAPPINGS = {
|
|
eq: 'eq',
|
|
neq: 'neq',
|
|
gt: 'gt',
|
|
gte: 'gte',
|
|
lt: 'lt',
|
|
lte: 'lte',
|
|
like: 'like',
|
|
'like(all)': 'likeAllOf',
|
|
'like(any)': 'likeAnyOf',
|
|
ilike: 'ilike',
|
|
'ilike(all)': 'ilikeAllOf',
|
|
'ilike(any)': 'ilikeAnyOf',
|
|
is: 'is',
|
|
in: 'in',
|
|
cs: 'contains',
|
|
cd: 'containedBy',
|
|
sr: 'rangeGt',
|
|
nxl: 'rangeGte',
|
|
sl: 'rangeLt',
|
|
nxr: 'rangeLte',
|
|
adj: 'rangeAdjacent',
|
|
ov: 'overlaps',
|
|
fts: '',
|
|
plfts: 'plain',
|
|
phfts: 'phrase',
|
|
wfts: 'websearch',
|
|
not: 'not',
|
|
};
|
|
|
|
const DB_OPERATIONS_TO_INSTRUMENT = ['select', 'insert', 'upsert', 'update', 'delete'];
|
|
|
|
function markAsInstrumented(fn) {
|
|
try {
|
|
(fn ).__SENTRY_INSTRUMENTED__ = true;
|
|
} catch {
|
|
// ignore errors here
|
|
}
|
|
}
|
|
|
|
function isInstrumented(fn) {
|
|
try {
|
|
return (fn ).__SENTRY_INSTRUMENTED__;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts the database operation type from the HTTP method and headers
|
|
* @param method - The HTTP method of the request
|
|
* @param headers - The request headers
|
|
* @returns The database operation type ('select', 'insert', 'upsert', 'update', or 'delete')
|
|
*/
|
|
function extractOperation(method, headers = {}) {
|
|
switch (method) {
|
|
case 'GET': {
|
|
return 'select';
|
|
}
|
|
case 'POST': {
|
|
if (headers['Prefer']?.includes('resolution=')) {
|
|
return 'upsert';
|
|
} else {
|
|
return 'insert';
|
|
}
|
|
}
|
|
case 'PATCH': {
|
|
return 'update';
|
|
}
|
|
case 'DELETE': {
|
|
return 'delete';
|
|
}
|
|
default: {
|
|
return '<unknown-op>';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Translates Supabase filter parameters into readable method names for tracing
|
|
* @param key - The filter key from the URL search parameters
|
|
* @param query - The filter value from the URL search parameters
|
|
* @returns A string representation of the filter as a method call
|
|
*/
|
|
function translateFiltersIntoMethods(key, query) {
|
|
if (query === '' || query === '*') {
|
|
return 'select(*)';
|
|
}
|
|
|
|
if (key === 'select') {
|
|
return `select(${query})`;
|
|
}
|
|
|
|
if (key === 'or' || key.endsWith('.or')) {
|
|
return `${key}${query}`;
|
|
}
|
|
|
|
const [filter, ...value] = query.split('.');
|
|
|
|
let method;
|
|
// Handle optional `configPart` of the filter
|
|
if (filter?.startsWith('fts')) {
|
|
method = 'textSearch';
|
|
} else if (filter?.startsWith('plfts')) {
|
|
method = 'textSearch[plain]';
|
|
} else if (filter?.startsWith('phfts')) {
|
|
method = 'textSearch[phrase]';
|
|
} else if (filter?.startsWith('wfts')) {
|
|
method = 'textSearch[websearch]';
|
|
} else {
|
|
method = (filter && FILTER_MAPPINGS[filter ]) || 'filter';
|
|
}
|
|
|
|
return `${method}(${key}, ${value.join('.')})`;
|
|
}
|
|
|
|
function instrumentAuthOperation(operation, isAdmin = false) {
|
|
return new Proxy(operation, {
|
|
apply(target, thisArg, argumentsList) {
|
|
return trace.startSpan(
|
|
{
|
|
name: `auth ${isAdmin ? '(admin) ' : ''}${operation.name}`,
|
|
attributes: {
|
|
[semanticAttributes.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase',
|
|
[semanticAttributes.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db',
|
|
'db.system': 'postgresql',
|
|
'db.operation': `auth.${isAdmin ? 'admin.' : ''}${operation.name}`,
|
|
},
|
|
},
|
|
span => {
|
|
return Reflect.apply(target, thisArg, argumentsList)
|
|
.then((res) => {
|
|
if (res && typeof res === 'object' && 'error' in res && res.error) {
|
|
span.setStatus({ code: spanstatus.SPAN_STATUS_ERROR });
|
|
|
|
exports$1.captureException(res.error, {
|
|
mechanism: {
|
|
handled: false,
|
|
},
|
|
});
|
|
} else {
|
|
span.setStatus({ code: spanstatus.SPAN_STATUS_OK });
|
|
}
|
|
|
|
span.end();
|
|
return res;
|
|
})
|
|
.catch((err) => {
|
|
span.setStatus({ code: spanstatus.SPAN_STATUS_ERROR });
|
|
span.end();
|
|
|
|
exports$1.captureException(err, {
|
|
mechanism: {
|
|
handled: false,
|
|
},
|
|
});
|
|
|
|
throw err;
|
|
})
|
|
.then(...argumentsList);
|
|
},
|
|
);
|
|
},
|
|
});
|
|
}
|
|
|
|
function instrumentSupabaseAuthClient(supabaseClientInstance) {
|
|
const auth = supabaseClientInstance.auth;
|
|
|
|
if (!auth || isInstrumented(supabaseClientInstance.auth)) {
|
|
return;
|
|
}
|
|
|
|
for (const operation of AUTH_OPERATIONS_TO_INSTRUMENT) {
|
|
const authOperation = auth[operation];
|
|
|
|
if (!authOperation) {
|
|
continue;
|
|
}
|
|
|
|
if (typeof supabaseClientInstance.auth[operation] === 'function') {
|
|
supabaseClientInstance.auth[operation] = instrumentAuthOperation(authOperation);
|
|
}
|
|
}
|
|
|
|
for (const operation of AUTH_ADMIN_OPERATIONS_TO_INSTRUMENT) {
|
|
const authOperation = auth.admin[operation];
|
|
|
|
if (!authOperation) {
|
|
continue;
|
|
}
|
|
|
|
if (typeof supabaseClientInstance.auth.admin[operation] === 'function') {
|
|
supabaseClientInstance.auth.admin[operation] = instrumentAuthOperation(authOperation, true);
|
|
}
|
|
}
|
|
|
|
markAsInstrumented(supabaseClientInstance.auth);
|
|
}
|
|
|
|
function instrumentSupabaseClientConstructor(SupabaseClient) {
|
|
if (isInstrumented((SupabaseClient ).prototype.from)) {
|
|
return;
|
|
}
|
|
|
|
(SupabaseClient ).prototype.from = new Proxy(
|
|
(SupabaseClient ).prototype.from,
|
|
{
|
|
apply(target, thisArg, argumentsList) {
|
|
const rv = Reflect.apply(target, thisArg, argumentsList);
|
|
const PostgRESTQueryBuilder = (rv ).constructor;
|
|
|
|
instrumentPostgRESTQueryBuilder(PostgRESTQueryBuilder );
|
|
|
|
return rv;
|
|
},
|
|
},
|
|
);
|
|
|
|
markAsInstrumented((SupabaseClient ).prototype.from);
|
|
}
|
|
|
|
function instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder) {
|
|
if (isInstrumented((PostgRESTFilterBuilder.prototype ).then)) {
|
|
return;
|
|
}
|
|
|
|
(PostgRESTFilterBuilder.prototype ).then = new Proxy(
|
|
(PostgRESTFilterBuilder.prototype ).then,
|
|
{
|
|
apply(target, thisArg, argumentsList) {
|
|
const operations = DB_OPERATIONS_TO_INSTRUMENT;
|
|
const typedThis = thisArg ;
|
|
const operation = extractOperation(typedThis.method, typedThis.headers);
|
|
|
|
if (!operations.includes(operation)) {
|
|
return Reflect.apply(target, thisArg, argumentsList);
|
|
}
|
|
|
|
if (!typedThis?.url?.pathname || typeof typedThis.url.pathname !== 'string') {
|
|
return Reflect.apply(target, thisArg, argumentsList);
|
|
}
|
|
|
|
const pathParts = typedThis.url.pathname.split('/');
|
|
const table = pathParts.length > 0 ? pathParts[pathParts.length - 1] : '';
|
|
|
|
const queryItems = [];
|
|
for (const [key, value] of typedThis.url.searchParams.entries()) {
|
|
// It's possible to have multiple entries for the same key, eg. `id=eq.7&id=eq.3`,
|
|
// so we need to use array instead of object to collect them.
|
|
queryItems.push(translateFiltersIntoMethods(key, value));
|
|
}
|
|
const body = Object.create(null);
|
|
if (is.isPlainObject(typedThis.body)) {
|
|
for (const [key, value] of Object.entries(typedThis.body)) {
|
|
body[key] = value;
|
|
}
|
|
}
|
|
|
|
// Adding operation to the beginning of the description if it's not a `select` operation
|
|
// For example, it can be an `insert` or `update` operation but the query can be `select(...)`
|
|
// For `select` operations, we don't need repeat it in the description
|
|
const description = `${operation === 'select' ? '' : `${operation}${body ? '(...) ' : ''}`}${queryItems.join(
|
|
' ',
|
|
)} from(${table})`;
|
|
|
|
const attributes = {
|
|
'db.table': table,
|
|
'db.schema': typedThis.schema,
|
|
'db.url': typedThis.url.origin,
|
|
'db.sdk': typedThis.headers['X-Client-Info'],
|
|
'db.system': 'postgresql',
|
|
'db.operation': operation,
|
|
[semanticAttributes.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.supabase',
|
|
[semanticAttributes.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db',
|
|
};
|
|
|
|
if (queryItems.length) {
|
|
attributes['db.query'] = queryItems;
|
|
}
|
|
|
|
if (Object.keys(body).length) {
|
|
attributes['db.body'] = body;
|
|
}
|
|
|
|
return trace.startSpan(
|
|
{
|
|
name: description,
|
|
attributes,
|
|
},
|
|
span => {
|
|
return (Reflect.apply(target, thisArg, []) )
|
|
.then(
|
|
(res) => {
|
|
if (span) {
|
|
if (res && typeof res === 'object' && 'status' in res) {
|
|
spanstatus.setHttpStatus(span, res.status || 500);
|
|
}
|
|
span.end();
|
|
}
|
|
|
|
if (res.error) {
|
|
const err = new Error(res.error.message) ;
|
|
if (res.error.code) {
|
|
err.code = res.error.code;
|
|
}
|
|
if (res.error.details) {
|
|
err.details = res.error.details;
|
|
}
|
|
|
|
const supabaseContext = {};
|
|
if (queryItems.length) {
|
|
supabaseContext.query = queryItems;
|
|
}
|
|
if (Object.keys(body).length) {
|
|
supabaseContext.body = body;
|
|
}
|
|
|
|
exports$1.captureException(err, {
|
|
contexts: {
|
|
supabase: supabaseContext,
|
|
},
|
|
});
|
|
}
|
|
|
|
const breadcrumb = {
|
|
type: 'supabase',
|
|
category: `db.${operation}`,
|
|
message: description,
|
|
};
|
|
|
|
const data = {};
|
|
|
|
if (queryItems.length) {
|
|
data.query = queryItems;
|
|
}
|
|
|
|
if (Object.keys(body).length) {
|
|
data.body = body;
|
|
}
|
|
|
|
if (Object.keys(data).length) {
|
|
breadcrumb.data = data;
|
|
}
|
|
|
|
breadcrumbs.addBreadcrumb(breadcrumb);
|
|
|
|
return res;
|
|
},
|
|
(err) => {
|
|
if (span) {
|
|
spanstatus.setHttpStatus(span, 500);
|
|
span.end();
|
|
}
|
|
throw err;
|
|
},
|
|
)
|
|
.then(...argumentsList);
|
|
},
|
|
);
|
|
},
|
|
},
|
|
);
|
|
|
|
markAsInstrumented((PostgRESTFilterBuilder.prototype ).then);
|
|
}
|
|
|
|
function instrumentPostgRESTQueryBuilder(PostgRESTQueryBuilder) {
|
|
// We need to wrap _all_ operations despite them sharing the same `PostgRESTFilterBuilder`
|
|
// constructor, as we don't know which method will be called first, and we don't want to miss any calls.
|
|
for (const operation of DB_OPERATIONS_TO_INSTRUMENT) {
|
|
if (isInstrumented((PostgRESTQueryBuilder.prototype )[operation])) {
|
|
continue;
|
|
}
|
|
|
|
(PostgRESTQueryBuilder.prototype )[operation ] = new Proxy(
|
|
(PostgRESTQueryBuilder.prototype )[operation ],
|
|
{
|
|
apply(target, thisArg, argumentsList) {
|
|
const rv = Reflect.apply(target, thisArg, argumentsList);
|
|
const PostgRESTFilterBuilder = (rv ).constructor;
|
|
|
|
debugBuild.DEBUG_BUILD && debugLogger.debug.log(`Instrumenting ${operation} operation's PostgRESTFilterBuilder`);
|
|
|
|
instrumentPostgRESTFilterBuilder(PostgRESTFilterBuilder);
|
|
|
|
return rv;
|
|
},
|
|
},
|
|
);
|
|
|
|
markAsInstrumented((PostgRESTQueryBuilder.prototype )[operation]);
|
|
}
|
|
}
|
|
|
|
const instrumentSupabaseClient = (supabaseClient) => {
|
|
if (!supabaseClient) {
|
|
debugBuild.DEBUG_BUILD && debugLogger.debug.warn('Supabase integration was not installed because no Supabase client was provided.');
|
|
return;
|
|
}
|
|
const SupabaseClientConstructor =
|
|
supabaseClient.constructor === Function ? supabaseClient : supabaseClient.constructor;
|
|
|
|
instrumentSupabaseClientConstructor(SupabaseClientConstructor);
|
|
instrumentSupabaseAuthClient(supabaseClient );
|
|
};
|
|
|
|
const INTEGRATION_NAME = 'Supabase';
|
|
|
|
const _supabaseIntegration = ((supabaseClient) => {
|
|
return {
|
|
setupOnce() {
|
|
instrumentSupabaseClient(supabaseClient);
|
|
},
|
|
name: INTEGRATION_NAME,
|
|
};
|
|
}) ;
|
|
|
|
const supabaseIntegration = integration.defineIntegration((options) => {
|
|
return _supabaseIntegration(options.supabaseClient);
|
|
}) ;
|
|
|
|
exports.DB_OPERATIONS_TO_INSTRUMENT = DB_OPERATIONS_TO_INSTRUMENT;
|
|
exports.FILTER_MAPPINGS = FILTER_MAPPINGS;
|
|
exports.extractOperation = extractOperation;
|
|
exports.instrumentSupabaseClient = instrumentSupabaseClient;
|
|
exports.supabaseIntegration = supabaseIntegration;
|
|
exports.translateFiltersIntoMethods = translateFiltersIntoMethods;
|
|
//# sourceMappingURL=supabase.js.map
|