feat: introduce extra telemetry info for teams and backend operations

This commit is contained in:
Andrew Bastin
2022-09-30 12:57:15 +05:30
parent 1b23c5ea4a
commit fb65e0e23d
6 changed files with 110 additions and 186 deletions

View File

@@ -1,4 +1,3 @@
// TODO: fix cache
import { ref } from "vue" import { ref } from "vue"
import { import {
createClient, createClient,
@@ -9,10 +8,11 @@ import {
makeOperation, makeOperation,
createRequest, createRequest,
subscriptionExchange, subscriptionExchange,
errorExchange,
CombinedError,
Operation,
} from "@urql/core" } from "@urql/core"
import { authExchange } from "@urql/exchange-auth" import { authExchange } from "@urql/exchange-auth"
// import { offlineExchange } from "@urql/exchange-graphcache"
// import { makeDefaultStorage } from "@urql/exchange-graphcache/default-storage"
import { devtoolsExchange } from "@urql/devtools" import { devtoolsExchange } from "@urql/devtools"
import { SubscriptionClient } from "subscriptions-transport-ws" import { SubscriptionClient } from "subscriptions-transport-ws"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
@@ -20,11 +20,6 @@ import * as TE from "fp-ts/TaskEither"
import { pipe, constVoid, flow } from "fp-ts/function" import { pipe, constVoid, flow } from "fp-ts/function"
import { subscribe, pipe as wonkaPipe } from "wonka" import { subscribe, pipe as wonkaPipe } from "wonka"
import { filter, map, Subject } from "rxjs" import { filter, map, Subject } from "rxjs"
// import { keyDefs } from "./caching/keys"
// import { optimisticDefs } from "./caching/optimistic"
// import { updatesDef } from "./caching/updates"
// import { resolversDef } from "./caching/resolvers"
// import schema from "./backend-schema.json"
import { import {
authIdToken$, authIdToken$,
getAuthIDToken, getAuthIDToken,
@@ -32,15 +27,25 @@ import {
waitProbableLoginToConfirm, waitProbableLoginToConfirm,
} from "~/helpers/fb/auth" } from "~/helpers/fb/auth"
// TODO: Implement caching
const BACKEND_GQL_URL = const BACKEND_GQL_URL =
import.meta.env.VITE_BACKEND_GQL_URL ?? "https://api.hoppscotch.io/graphql" import.meta.env.VITE_BACKEND_GQL_URL ?? "https://api.hoppscotch.io/graphql"
const BACKEND_WS_URL = const BACKEND_WS_URL =
import.meta.env.VITE_BACKEND_WS_URL ?? "wss://api.hoppscotch.io/graphql" import.meta.env.VITE_BACKEND_WS_URL ?? "wss://api.hoppscotch.io/graphql"
// const storage = makeDefaultStorage({ /**
// idbName: "hoppcache-v1", * A type that defines error events that are possible during backend operations on the GQLCLient
// maxAge: 7, */
// }) export type GQLClientErrorEvent =
| { type: "SUBSCRIPTION_CONN_CALLBACK_ERR_REPORT"; errors: Error[] }
| { type: "CLIENT_REPORTED_ERROR"; error: CombinedError; op: Operation }
/**
* A stream of the errors that occur during GQLClient operations.
* Exposed to be subscribed to by systems like sentry for error reporting
*/
export const gqlClientError$ = new Subject<GQLClientErrorEvent>()
const subscriptionClient = new SubscriptionClient(BACKEND_WS_URL, { const subscriptionClient = new SubscriptionClient(BACKEND_WS_URL, {
reconnect: true, reconnect: true,
@@ -49,6 +54,14 @@ const subscriptionClient = new SubscriptionClient(BACKEND_WS_URL, {
authorization: `Bearer ${authIdToken$.value}`, authorization: `Bearer ${authIdToken$.value}`,
} }
}, },
connectionCallback(error) {
if (error?.length > 0) {
gqlClientError$.next({
type: "SUBSCRIPTION_CONN_CALLBACK_ERR_REPORT",
errors: error,
})
}
},
}) })
authIdToken$.subscribe(() => { authIdToken$.subscribe(() => {
@@ -61,14 +74,6 @@ const createHoppClient = () =>
exchanges: [ exchanges: [
devtoolsExchange, devtoolsExchange,
dedupExchange, dedupExchange,
// offlineExchange({
// schema: schema as any,
// keys: keyDefs,
// optimistic: optimisticDefs,
// updates: updatesDef,
// resolvers: resolversDef,
// storage,
// }),
authExchange({ authExchange({
addAuthToOperation({ authState, operation }) { addAuthToOperation({ authState, operation }) {
if (!authState || !authState.authToken) { if (!authState || !authState.authToken) {
@@ -109,6 +114,15 @@ const createHoppClient = () =>
forwardSubscription: (operation) => forwardSubscription: (operation) =>
subscriptionClient.request(operation), subscriptionClient.request(operation),
}), }),
errorExchange({
onError(error, op) {
gqlClientError$.next({
type: "CLIENT_REPORTED_ERROR",
error,
op,
})
},
}),
], ],
}) })

View File

@@ -1,9 +0,0 @@
import { GraphCacheKeysConfig } from "../graphql"
export const keyDefs: GraphCacheKeysConfig = {
User: (data) => data.uid!,
TeamMember: (data) => data.membershipID!,
Team: (data) => data.id!,
Shortcode: (data) => data.id!,
TeamInvitation: (data) => data.id!,
}

View File

@@ -1,3 +0,0 @@
import { GraphCacheOptimisticUpdaters } from "../graphql"
export const optimisticDefs: GraphCacheOptimisticUpdaters = {}

View File

@@ -1,31 +0,0 @@
import {
GraphCacheResolvers,
Shortcode,
Team,
TeamInvitation,
WithTypename,
} from "../graphql"
export const resolversDef: GraphCacheResolvers = {
Query: {
team: (_parent, { teamID }) =>
<WithTypename<Team>>{
__typename: "Team" as const,
id: teamID,
},
user: (_parent, { uid }) => ({
__typename: "User",
uid,
}),
teamInvitation: (_parent, args) =>
<WithTypename<TeamInvitation>>{
__typename: "TeamInvitation",
id: args.inviteID,
},
shortcode: (_parent, args) =>
<WithTypename<Shortcode>>{
__typename: "Shortcode",
id: args.code,
},
},
}

View File

@@ -1,122 +0,0 @@
import { gql } from "@urql/core"
import { GraphCacheUpdaters } from "../graphql"
export const updatesDef: GraphCacheUpdaters = {
Subscription: {
teamMemberAdded: (_r, { teamID }, cache) => {
cache.invalidate(
{
__typename: "Team",
id: teamID,
},
"teamMembers"
)
},
teamMemberUpdated: (_r, { teamID }, cache) => {
cache.invalidate(
{
__typename: "Team",
id: teamID,
},
"teamMembers"
)
cache.invalidate(
{
__typename: "Team",
id: teamID,
},
"myRole"
)
},
teamMemberRemoved: (_r, { teamID }, cache) => {
cache.invalidate(
{
__typename: "Team",
id: teamID,
},
"teamMembers"
)
},
teamInvitationAdded: (_r, { teamID }, cache) => {
cache.invalidate(
{
__typename: "Team",
id: teamID,
},
"teamInvitations"
)
},
teamInvitationRemoved: (_r, { teamID }, cache) => {
cache.invalidate(
{
__typename: "Team",
id: teamID,
},
"teamInvitations"
)
},
},
Mutation: {
createTeamInvitation: (result, _args, cache) => {
cache.invalidate(
{
__typename: "Team",
id: result.createTeamInvitation.teamID!,
},
"teamInvitations"
)
},
acceptTeamInvitation: (_result, _args, cache) => {
cache.invalidate({ __typename: "Query" }, "myTeams")
},
revokeTeamInvitation: (_result, args, cache) => {
const targetTeamID = cache.resolve(
{
__typename: "TeamInvitation",
id: args.inviteID,
},
"teamID"
)
if (typeof targetTeamID === "string") {
const newInvites = (
cache.resolve(
{
__typename: "Team",
id: targetTeamID,
},
"teamInvitations"
) as string[]
).filter(
(inviteKey) =>
inviteKey !==
cache.keyOfEntity({
__typename: "TeamInvitation",
id: args.inviteID,
})
)
cache.link(
{ __typename: "Team", id: targetTeamID },
"teamInvitations",
newInvites
)
}
},
createShortcode: (result, _args, cache) => {
cache.writeFragment(
gql`
fragment _ on Shortcode {
id
request
}
`,
{
id: result.createShortcode.id,
request: result.createShortcode.request,
}
)
},
},
}

View File

@@ -6,6 +6,12 @@ import { RouteLocationNormalized, Router } from "vue-router"
import { settingsStore } from "~/newstore/settings" import { settingsStore } from "~/newstore/settings"
import { App } from "vue" import { App } from "vue"
import { APP_IS_IN_DEV_MODE } from "~/helpers/dev" import { APP_IS_IN_DEV_MODE } from "~/helpers/dev"
import { gqlClientError$ } from "~/helpers/backend/GQLClient"
/**
* The tag names we allow giving to Sentry
*/
type SentryTag = "BACKEND_OPERATIONS"
interface SentryVueRouter { interface SentryVueRouter {
onError: (fn: (err: Error) => void) => void onError: (fn: (err: Error) => void) => void
@@ -44,7 +50,7 @@ let sentryActive = false
function initSentry(dsn: string, router: Router, app: App) { function initSentry(dsn: string, router: Router, app: App) {
Sentry.init({ Sentry.init({
app, app,
dsn: import.meta.env.VITE_SENTRY_DSN, dsn,
environment: APP_IS_IN_DEV_MODE environment: APP_IS_IN_DEV_MODE
? "dev" ? "dev"
: import.meta.env.VITE_SENTRY_ENVIRONMENT, : import.meta.env.VITE_SENTRY_ENVIRONMENT,
@@ -67,6 +73,73 @@ function deinitSentry() {
sentryActive = false sentryActive = false
} }
/**
* Reports a set of related errors to Sentry
* @param errs The errors to report
* @param tag The tag for the errord
* @param extraTags Additional tag data to add
* @param extras Extra information to attach
*/
function reportErrors(
errs: Error[],
tag: SentryTag,
extraTags: Record<string, string | number | boolean> | null = null,
extras: any = undefined
) {
if (sentryActive) {
Sentry.withScope((scope) => {
scope.setTag("tag", tag)
if (extraTags) {
Object.entries(extraTags).forEach(([key, value]) => {
scope.setTag(key, value)
})
}
if (extras !== null && extras === undefined) scope.setExtras(extras)
errs.forEach((err) => Sentry.captureException(err))
})
}
}
/**
* Reports a specific error to Sentry
* @param err The error to report
* @param tag The tag for the error
* @param extraTags Additional tag data to add
* @param extras Extra information to attach
*/
function reportError(
err: Error,
tag: SentryTag,
extraTags: Record<string, string | number | boolean> | null = null,
extras: any = undefined
) {
reportErrors([err], tag, extraTags, extras)
}
/**
* Subscribes to events occuring in various subsystems in the app
* for personalized error reporting
*/
function subscribeToAppEventsForReporting() {
gqlClientError$.subscribe((ev) => {
switch (ev.type) {
case "SUBSCRIPTION_CONN_CALLBACK_ERR_REPORT":
reportErrors(ev.errors, "BACKEND_OPERATIONS", { from: ev.type })
break
case "CLIENT_REPORTED_ERROR":
reportError(
ev.error,
"BACKEND_OPERATIONS",
{ from: ev.type },
{ op: ev.op }
)
break
}
})
}
export default <HoppModule>{ export default <HoppModule>{
onRouterInit(app, router) { onRouterInit(app, router) {
if (!import.meta.env.VITE_SENTRY_DSN) { if (!import.meta.env.VITE_SENTRY_DSN) {
@@ -87,5 +160,7 @@ export default <HoppModule>{
initSentry(import.meta.env.VITE_SENTRY_DSN!, router, app) initSentry(import.meta.env.VITE_SENTRY_DSN!, router, app)
} }
}) })
subscribeToAppEventsForReporting()
}, },
} }