feat: introduce extra telemetry info for teams and backend operations
This commit is contained in:
@@ -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,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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!,
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { GraphCacheOptimisticUpdaters } from "../graphql"
|
|
||||||
|
|
||||||
export const optimisticDefs: GraphCacheOptimisticUpdaters = {}
|
|
||||||
@@ -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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user