Skip to content

Commit

Permalink
Merge pull request #4 from fumeapp/support-nuxt-auth-utils
Browse files Browse the repository at this point in the history
✨ support storing user from nuxt-auth-utils
  • Loading branch information
acidjazz authored Jan 27, 2025
2 parents 33230e8 + 2df8e96 commit 5df80fb
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 29 deletions.
55 changes: 40 additions & 15 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,50 @@
import { defineNuxtModule, addPlugin, addServerPlugin, createResolver, useRuntimeConfig } from '@nuxt/kit'
import { careReportConfig } from './runtime/care'
import { careReportConfig, careConfigDefaults } from './runtime/care'

// Module options TypeScript interface definition
export interface ModuleOptions {
apiKey: string
apiDomain?: string
verbose?: boolean
apiDomain: string
verbose: boolean
userFromAuthUtils: boolean
authUtilsUserFields: string[]
}

declare module 'nuxt/schema' {
interface PubilcRuntimeConfig {
// API key for Care
apiKey: string
// Optional custom care API domain
apiDomain?: string
// Verbose logging
verbose?: boolean
care: {
/**
* fume.care API Key
*
*/
apiKey: string
/**
* Optional custom fume.care API domain
*
* @default https://fume.care
*/

apiDomain?: string
/**
* Verbose logging
*
* @default false
*/
verbose?: boolean
/**
* Attempt to store the user from nuxt-auth-utils
* @see https://nuxt.com/modules/auth-utils
*
* @default false
*/

userFromAuthUtils?: boolean
/**
* Customize the fields that are plucked from the user supplied from nuxt-auth-utils
*
* @default ['id', 'email', 'name', 'avatar']
*/
authUtilsUserFields?: string[]
}
}
}

Expand All @@ -24,11 +53,7 @@ export default defineNuxtModule<ModuleOptions>({
name: 'fume.care',
configKey: 'care',
},
// Default configuration options of the Nuxt module
defaults: {
apiDomain: 'https://fume.care',
verbose: false,
},
defaults: careConfigDefaults,
setup(options, nuxt) {
const resolver = createResolver(import.meta.url)
const config = useRuntimeConfig().public.care || options
Expand Down
84 changes: 71 additions & 13 deletions src/runtime/care.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import log from 'consola'
import type { ComputedRef } from 'vue'
import type { H3Event } from 'h3'
import type { ModuleOptions as Config } from '../module'

interface ErrorPayload {
Expand All @@ -14,21 +16,79 @@ interface ErrorPayload {
version: string
}
}

interface ErrorMeta {
user?: {
id?: string
email?: string
name?: string
avatar?: string
}
meta?: Record<string, unknown>
}

export const careConfigDefaults = {
apiDomain: 'https://fume.care',
verbose: false,
userFromAuthUtils: false,
authUtilsUserFields: ['id', 'email', 'name', 'avatar'],
}

const mergeConfig = (config: Config) => {
return {
...careConfigDefaults,
...config,
}
}

declare const useUserSession: () => { user: ComputedRef<Record<string, unknown>> }
declare const getUserSession: (event: H3Event) => Promise<{ user: Record<string, unknown> }>

export enum CareHookType {
vueError = 'vue:error',
appError = 'app:error',
nitroError = 'nitro:error',
windowRejection = 'window:unhandledrejection',
}

const userFromFields = (user: Record<string, unknown>, fields: string[]) => {
if (!user || !fields?.length) return undefined

const filtered = Object.fromEntries(
fields
.filter(key => user[key] !== undefined)
.map(key => [key, user[key]]),
)

return Object.keys(filtered).length ? filtered : undefined
}

const getMeta = async (config: Config, event?: H3Event) => {
const meta: ErrorMeta = { user: undefined, meta: undefined }
if (config.userFromAuthUtils && typeof useUserSession === 'function') {
const { user } = useUserSession()
meta.user = userFromFields(user.value, config.authUtilsUserFields)
}

if (config.userFromAuthUtils && event && typeof getUserSession === 'function') {
const { user } = await getUserSession(event)
meta.user = userFromFields(user, config.authUtilsUserFields)
}
if (config.verbose) log.info('[fume.care] Meta:', meta)
return meta
}

const validApiKey = (config: Config): boolean => {
if (!config || !config.apiKey) return false
return /^[a-z0-9]{32}$/.test(config.apiKey)
return /^[a-z0-9]{32}$/i.test(config.apiKey)
}

export const careReportConfig = (config: Config) => {
if (!validApiKey(config)) {
log.info('[fume.care] No valid API Key discovered - reporting muted')
if (!config.apiKey) {
log.info('[fume.care] no API key detected - reporting muted')
}
else if (!validApiKey(config)) {
log.warn('[fume.care] API key is invalid - reporting muted')
}
else {
log.success('[fume.care] Valid API key found - reporting activated')
Expand All @@ -43,17 +103,14 @@ export const careCheckConfig = (config: Config): boolean => {
return validApiKey(config)
}

export const careReport = (type: CareHookType, error: unknown, config: Config) => {
console.log('calling careReport')
sendError(type, error as ErrorPayload, config)
}

const sendError = async (hook: string, error: ErrorPayload, config: Config) => {
export const careReport = async (type: CareHookType, err: unknown, unmerged: Config, event?: H3Event) => {
const config = mergeConfig(unmerged)
const error = err as ErrorPayload
const payload: ErrorPayload = {
name: error.name,
message: error.message,
stack: error.stack,
hook: hook,
hook: type,
cause: error.cause,
client: typeof window !== 'undefined',
os: {
Expand All @@ -65,7 +122,7 @@ const sendError = async (hook: string, error: ErrorPayload, config: Config) => {
const url = `${config.apiDomain}/api/issue`
try {
if (config.verbose) {
log.info(`[fume.care] Error in ${hook} going to ${url}`, payload)
log.info(`[fume.care] Error in ${type} going to ${url}`, payload)
}
const response = await fetch(url, {
method: 'POST',
Expand All @@ -74,13 +131,14 @@ const sendError = async (hook: string, error: ErrorPayload, config: Config) => {
apiKey: config.apiKey,
environment: 'production',
payload: JSON.stringify(payload),
meta: JSON.stringify(await getMeta(config, event)),
}),
})
const data = await response.json()
log.success('[fume.care] Error sent successfully:', data.meta)
if (config.verbose) log.success('[fume.care] Error sent successfully:', data.meta)
return data
}
catch (err) {
log.error(`[fume.care] Failed to send error to ${url}:`, err)
if (config.verbose) log.error(`[fume.care] Failed to send error to ${url}:`, err)
}
}
2 changes: 1 addition & 1 deletion src/runtime/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import { CareHookType, careCheckConfig, careReport } from './care'
export default defineNitroPlugin((nitroApp) => {
const config = useRuntimeConfig().public.care as Required<ModuleOptions>
if (careCheckConfig(config))
nitroApp.hooks.hook('error', async error => careReport(CareHookType.nitroError, error, config))
nitroApp.hooks.hook('error', async (error, { event }) => careReport(CareHookType.nitroError, error, config, event))
})

0 comments on commit 5df80fb

Please sign in to comment.