Skip to content
This repository was archived by the owner on May 11, 2026. It is now read-only.
Open
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
10 changes: 1 addition & 9 deletions src/graphql/operations/aliases.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import log from '../../helpers/log';
import db from '../../helpers/mysql';
import { buildWhereQuery, checkLimits } from '../helpers';

Expand Down Expand Up @@ -33,11 +31,5 @@ export default async function (parent, args) {
`;
params.push(skip, first);

try {
return await db.queryAsync(query, params);
} catch (e: any) {
log.error(`[graphql] aliases, ${JSON.stringify(e)}`);
capture(e, { args });
return Promise.reject(new Error('request failed'));
}
return await db.queryAsync(query, params);
}
12 changes: 2 additions & 10 deletions src/graphql/operations/follows.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import log from '../../helpers/log';
import db from '../../helpers/mysql';
import { buildWhereQuery, checkLimits, formatFollow } from '../helpers';

Expand Down Expand Up @@ -46,12 +44,6 @@ export default async function (parent, args) {
ORDER BY ${orderBy} ${orderDirection} LIMIT ?, ?
`;

try {
const follows = await db.queryAsync(query, params);
return follows.map(follow => formatFollow(follow));
} catch (e: any) {
log.error(`[graphql] follows, ${JSON.stringify(e)}`);
capture(e, { args });
return Promise.reject(new Error('request failed'));
}
const follows = await db.queryAsync(query, params);
return follows.map(follow => formatFollow(follow));
}
19 changes: 18 additions & 1 deletion src/graphql/operations/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import aliases from './aliases';
import follows from './follows';
import leaderboards from './leaderboards';
Expand All @@ -23,8 +24,20 @@ import validations from './validations';
import vote from './vote';
import votes from './votes';
import vp from './vp';
import log from '../../helpers/log';

export default {
const IGNORED_ERROR_CODES = ['ER_QUERY_TIMEOUT'];

function withErrorHandler(fn) {
return (...args) =>
fn(...args).catch(e => {
if (!IGNORED_ERROR_CODES.includes(e.code)) capture(e);

Copilot AI Feb 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The centralized error handler no longer captures context information (args, context, info) that was previously passed to capture() in the individual resolvers. This context is valuable for debugging and error tracking in Sentry. The wrapper has access to the arguments passed to each resolver function through the ...args parameter, but these aren't being passed to capture().

Consider capturing the arguments in the error handler to preserve this debugging context.

Suggested change
if (!IGNORED_ERROR_CODES.includes(e.code)) capture(e);
if (!IGNORED_ERROR_CODES.includes(e.code)) {
const [, resolverArgs, context, info] = args;
capture(e, { args: resolverArgs, context, info });
}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this make sense as well. maybe we need them?

Comment thread
wa0x6e marked this conversation as resolved.
log.error(`[graphql] ${JSON.stringify(e)}`);

Copilot AI Feb 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The centralized error logging no longer includes the specific operation name that failed. Previously, each operation logged errors with a specific identifier like [graphql] votes, [graphql] proposals, etc. Now all errors are logged as just [graphql], making it harder to identify which operation failed without examining the full error object.

Consider including the operation name in the log message by adding it as a parameter to the wrapper or extracting it from the function name.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before we know which operation is failing, copilot is correct here

return Promise.reject(new Error('request failed'));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe need to figure out how we should handle custom error messages, for example vp.ts, user.ts

});
}
Comment on lines +31 to +38

Copilot AI Feb 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The withErrorHandler wrapper calls .catch() on all wrapped functions, but some operations (plugins, skins, validations, strategies) are synchronous and return values directly instead of Promises. Calling .catch() on a non-Promise value will cause a runtime error.

To fix this, the wrapper should handle both synchronous and asynchronous functions. Consider wrapping the function call in Promise.resolve() first, or check if the return value is a Promise before calling .catch().

Copilot uses AI. Check for mistakes.

const operations = {
space,
spaces,
ranking,
Expand All @@ -51,3 +64,7 @@ export default {
roles,
leaderboards
};

export default Object.fromEntries(
Object.entries(operations).map(([key, fn]) => [key, withErrorHandler(fn)])

Copilot AI Feb 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The operations space.ts, spaces.ts, ranking.ts, and vote.ts still have their own try/catch blocks with error handling and logging. This means errors in these operations will be logged twice - once in their own try/catch and again in the withErrorHandler wrapper. Additionally, these operations return Error objects (not Promise.reject), which won't trigger the .catch() in the wrapper, so the centralized error handling won't apply to them at all.

Either remove the try/catch blocks from these files to use only the centralized handler, or remove them from the operations object so they aren't wrapped. If these operations need special error handling (like PublicError), that logic should be incorporated into the centralized handler or kept separate from the wrapper.

Suggested change
Object.entries(operations).map(([key, fn]) => [key, withErrorHandler(fn)])
Object.entries(operations).map(([key, fn]) => {
const skipWrapper = ['space', 'spaces', 'ranking', 'vote'];
return skipWrapper.includes(key) ? [key, fn] : [key, withErrorHandler(fn)];
})

Copilot uses AI. Check for mistakes.
);
10 changes: 1 addition & 9 deletions src/graphql/operations/leaderboards.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import uniq from 'lodash/uniq';
import log from '../../helpers/log';
import db from '../../helpers/mysql';
import { buildWhereQuery, checkLimits } from '../helpers';

Expand Down Expand Up @@ -50,11 +48,5 @@ export default async function (parent, args) {
ORDER BY ${orderQuery} LIMIT ?, ?
`;

try {
return db.queryAsync(query, [...whereQuery.params, skip, first]);
} catch (e) {
log.error(`[graphql] leaderboards, ${JSON.stringify(e)}`);
capture(e, { args });
return Promise.reject(new Error('request failed'));
}
return db.queryAsync(query, [...whereQuery.params, skip, first]);
}
10 changes: 1 addition & 9 deletions src/graphql/operations/messages.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import log from '../../helpers/log';
import { sequencerDB } from '../../helpers/mysql';
import { buildWhereQuery, checkLimits } from '../helpers';

Expand Down Expand Up @@ -33,11 +31,5 @@ export default async function (parent, args) {
ORDER BY ${orderBy} ${orderDirection} LIMIT ?, ?
`;
params.push(skip, first);
try {
return await sequencerDB.queryAsync(query, params);
} catch (e: any) {
log.error(`[graphql] messages, ${JSON.stringify(e)}`);
capture(e, { args });
return Promise.reject(new Error('request failed'));
}
return await sequencerDB.queryAsync(query, params);
}
12 changes: 2 additions & 10 deletions src/graphql/operations/proposal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import log from '../../helpers/log';
import db from '../../helpers/mysql';
import { formatProposal } from '../helpers';

Expand All @@ -22,12 +20,6 @@ export default async function (parent, { id }) {
WHERE p.id = ? AND spaces.settings IS NOT NULL
LIMIT 1
`;
try {
const proposals = await db.queryAsync(query, [id]);
return proposals.map(proposal => formatProposal(proposal))[0] || null;
} catch (e: any) {
log.error(`[graphql] proposal, ${JSON.stringify(e)}`);
capture(e, { id });
return Promise.reject(new Error('request failed'));
}
const proposals = await db.queryAsync(query, [id]);
return proposals.map(proposal => formatProposal(proposal))[0] || null;
}
12 changes: 2 additions & 10 deletions src/graphql/operations/proposals.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import log from '../../helpers/log';
import db from '../../helpers/mysql';
import { buildWhereQuery, checkLimits, formatProposal } from '../helpers';

Expand Down Expand Up @@ -109,12 +107,6 @@ export default async function (parent, args) {
ORDER BY ${orderBy} ${orderDirection}, p.id ASC LIMIT ?, ?
`;
params.push(skip, first);
try {
const proposals = await db.queryAsync(query, params);
return proposals.map(proposal => formatProposal(proposal));
} catch (e: any) {
log.error(`[graphql] proposals, ${JSON.stringify(e)}`);
capture(e, { args });
return Promise.reject(new Error('request failed'));
}
const proposals = await db.queryAsync(query, params);
return proposals.map(proposal => formatProposal(proposal));
}
46 changes: 19 additions & 27 deletions src/graphql/operations/roles.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import log from '../../helpers/log';
import db from '../../helpers/mysql';

export default async function (parent, args) {
Expand All @@ -13,31 +11,25 @@ export default async function (parent, args) {
OR JSON_CONTAINS(LOWER(settings->'$.moderators'), LOWER(JSON_QUOTE(?)));
`;

try {
const data = await db.queryAsync(query, [address, address, address]);
return data.map((space: any) => {
const settings = JSON.parse(space.settings);
const permissions: string[] = [];
const admins = (settings.admins || []).map((admin: string) =>
admin.toLowerCase()
);
const moderators = (settings.moderators || []).map((moderator: string) =>
moderator.toLowerCase()
);
const members = (settings.members || []).map((member: string) =>
member.toLowerCase()
);
const data = await db.queryAsync(query, [address, address, address]);
return data.map((space: any) => {
const settings = JSON.parse(space.settings);
Comment thread
wa0x6e marked this conversation as resolved.
const permissions: string[] = [];
const admins = (settings.admins || []).map((admin: string) =>
admin.toLowerCase()
);
const moderators = (settings.moderators || []).map((moderator: string) =>
moderator.toLowerCase()
);
const members = (settings.members || []).map((member: string) =>
member.toLowerCase()
);

if (admins.includes(address.toLowerCase())) permissions.push('admin');
if (moderators.includes(address.toLowerCase()))
permissions.push('moderator');
if (members.includes(address.toLowerCase())) permissions.push('author');
if (admins.includes(address.toLowerCase())) permissions.push('admin');
if (moderators.includes(address.toLowerCase()))
permissions.push('moderator');
if (members.includes(address.toLowerCase())) permissions.push('author');

return { space: space.id, permissions };
});
} catch (e: any) {
log.error(`[graphql] roles, ${JSON.stringify(e)}`);
capture(e, { args });
return Promise.reject(new Error('request failed'));
}
return { space: space.id, permissions };
});
}
14 changes: 3 additions & 11 deletions src/graphql/operations/statement.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import log from '../../helpers/log';
import db from '../../helpers/mysql';

export default async function (parent, args) {
const id = args.id;
const query = `SELECT s.* FROM statements s WHERE id = ? LIMIT 1`;
try {
const statements = await db.queryAsync(query, id);
if (statements.length === 1) return statements[0];
return null;
} catch (e: any) {
log.error(`[graphql] statement, ${JSON.stringify(e)}`);
capture(e, { args });
return Promise.reject(new Error('request failed'));
}
const statements = await db.queryAsync(query, id);
if (statements.length === 1) return statements[0];
return null;
}
12 changes: 2 additions & 10 deletions src/graphql/operations/statements.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import log from '../../helpers/log';
import db from '../../helpers/mysql';
import { buildWhereQuery, checkLimits } from '../helpers';

Expand Down Expand Up @@ -36,12 +34,6 @@ export default async function (parent, args) {
ORDER BY ${orderBy} ${orderDirection} LIMIT ?, ?
`;
params.push(skip, first);
try {
statements = await db.queryAsync(query, params);
return statements;
} catch (e: any) {
log.error(`[graphql] statements, ${JSON.stringify(e)}`);
capture(e, { args });
return Promise.reject(new Error('request failed'));
}
statements = await db.queryAsync(query, params);
return statements;
}
12 changes: 2 additions & 10 deletions src/graphql/operations/subscriptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import log from '../../helpers/log';
import db from '../../helpers/mysql';
import { buildWhereQuery, checkLimits, formatSubscription } from '../helpers';

Expand Down Expand Up @@ -48,12 +46,6 @@ export default async function (parent, args) {
`;
params.push(skip, first);

try {
subscriptions = await db.queryAsync(query, params);
return subscriptions.map(subscription => formatSubscription(subscription));
} catch (e: any) {
log.error(`[graphql] subscriptions, ${JSON.stringify(e)}`);
capture(e, { args });
return Promise.reject(new Error('request failed'));
}
subscriptions = await db.queryAsync(query, params);
return subscriptions.map(subscription => formatSubscription(subscription));
}
70 changes: 31 additions & 39 deletions src/graphql/operations/users.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { capture } from '@snapshot-labs/snapshot-sentry';
import log from '../../helpers/log';
import db from '../../helpers/mysql';
import { buildWhereQuery, checkLimits, formatUser } from '../helpers';

Expand Down Expand Up @@ -39,43 +37,37 @@ export default async function (parent, args) {
ORDER BY ${orderBy} ${orderDirection} LIMIT ?, ?
`;
params.push(skip, first);
try {
const users = await db.queryAsync(query, params);
ids.forEach(element => {
if (!users.find((u: any) => u.id === element)) {
users.push({ id: element });
}
});
if (!users.length) return [];

const usersWithOutCreated = users
.filter((u: any) => !u.created)
.map((u: any) => u.id);
if (usersWithOutCreated.length) {
const counts = await db.queryAsync(
`
SELECT
user,
COALESCE(SUM(vote_count), 0) as votesCount,
COALESCE(SUM(proposal_count) ,0) as proposalsCount,
MAX(last_vote) as lastVote
FROM leaderboard
WHERE user IN (?)
GROUP BY user
`,
[usersWithOutCreated]
);
counts.forEach((count: any) => {
const user = users.find((u: any) => u.id === count.user);
user.votesCount = count.votesCount;
user.proposalsCount = count.proposalsCount;
user.lastVote = count.lastVote;
});
const users = await db.queryAsync(query, params);
ids.forEach(element => {
if (!users.find((u: any) => u.id === element)) {
users.push({ id: element });
}
return users.map(formatUser);
} catch (e: any) {
log.error(`[graphql] users, ${JSON.stringify(e)}`);
capture(e, { args });
return Promise.reject(new Error('request failed'));
});
if (!users.length) return [];

const usersWithOutCreated = users
.filter((u: any) => !u.created)
.map((u: any) => u.id);
if (usersWithOutCreated.length) {
const counts = await db.queryAsync(
`
SELECT
user,
COALESCE(SUM(vote_count), 0) as votesCount,
COALESCE(SUM(proposal_count) ,0) as proposalsCount,
MAX(last_vote) as lastVote
FROM leaderboard
WHERE user IN (?)
GROUP BY user
`,
[usersWithOutCreated]
);
counts.forEach((count: any) => {
const user = users.find((u: any) => u.id === count.user);
user.votesCount = count.votesCount;
user.proposalsCount = count.proposalsCount;
user.lastVote = count.lastVote;
});
}
return users.map(formatUser);
}
Loading