@cooperco/nuxt-layer-base 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +287 -0
- package/app/composables/useLoggly.ts +28 -0
- package/app/plugins/loggly.client.ts +70 -0
- package/nuxt.config.ts +24 -2
- package/package.json +10 -6
- package/server/api/loggly.post.ts +58 -0
- package/server/plugins/loggly.ts +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Base Layer
|
|
2
|
+
|
|
3
|
+
A foundational layer that provides essential configuration and tooling for Nuxt 4 projects.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- TypeScript integration with strict mode enabled
|
|
8
|
+
- ESLint configuration with stylistic rules for Vue templates
|
|
9
|
+
- Test utilities integration
|
|
10
|
+
- Internationalization (i18n) via `@nuxtjs/i18n`
|
|
11
|
+
- Nuxt DevTools enabled
|
|
12
|
+
- CI/CD workflows for code quality checks
|
|
13
|
+
|
|
14
|
+
## Development
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Install dependencies
|
|
18
|
+
npm install
|
|
19
|
+
|
|
20
|
+
# Start development server
|
|
21
|
+
npm run dev
|
|
22
|
+
|
|
23
|
+
# Run ESLint
|
|
24
|
+
npm run lint
|
|
25
|
+
|
|
26
|
+
# Fix linting issues automatically
|
|
27
|
+
npm run lint:fix
|
|
28
|
+
|
|
29
|
+
# Run TypeScript type checking
|
|
30
|
+
npm run typecheck
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration Details
|
|
34
|
+
|
|
35
|
+
### Key Features
|
|
36
|
+
|
|
37
|
+
- **Compatibility Date:** 2025-07-15
|
|
38
|
+
- **Strict TypeScript:** Enforces type safety throughout your projects
|
|
39
|
+
- **ESLint Integration:** Pre-configured with stylistic rules for Vue templates
|
|
40
|
+
- Custom ESLint rules for code style consistency
|
|
41
|
+
- Vue template formatting rules
|
|
42
|
+
- **Testing Utilities:** Built-in test utils for comprehensive testing
|
|
43
|
+
|
|
44
|
+
### Configuration Details
|
|
45
|
+
|
|
46
|
+
The base layer includes:
|
|
47
|
+
|
|
48
|
+
- Nuxt ESLint module (`@nuxt/eslint`)
|
|
49
|
+
- Nuxt Test Utils module (`@nuxt/test-utils`)
|
|
50
|
+
- Nuxt i18n module (`@nuxtjs/i18n`)
|
|
51
|
+
- Nuxt DevTools enabled
|
|
52
|
+
- TypeScript configuration with strict mode
|
|
53
|
+
- Custom ESLint configuration with rules for:
|
|
54
|
+
- JavaScript stylistic preferences
|
|
55
|
+
- TypeScript unused variables handling
|
|
56
|
+
- Vue template formatting
|
|
57
|
+
|
|
58
|
+
## Internationalization (i18n)
|
|
59
|
+
|
|
60
|
+
This layer integrates the Nuxt i18n module (`@nuxtjs/i18n`). You can override or extend its configuration in your application.
|
|
61
|
+
|
|
62
|
+
- Docs: https://i18n.nuxtjs.org
|
|
63
|
+
|
|
64
|
+
Override example in a consuming app:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// nuxt.config.ts (in your app)
|
|
68
|
+
export default defineNuxtConfig({
|
|
69
|
+
extends: ['@cooperco/nuxt-layer-base'],
|
|
70
|
+
i18n: {
|
|
71
|
+
locales: [
|
|
72
|
+
{ code: 'en', language: 'en-US', name: 'English' },
|
|
73
|
+
{ code: 'fr', language: 'fr-FR', name: 'Français' }
|
|
74
|
+
],
|
|
75
|
+
defaultLocale: 'en'
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Basic usage in a component:
|
|
81
|
+
|
|
82
|
+
```vue
|
|
83
|
+
<script setup lang="ts">
|
|
84
|
+
const { t, locale } = useI18n()
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<p>{{ t('hello') }}</p>
|
|
89
|
+
</template>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Note: Provide your own translation files (for example, `locales/en.json`) in your application.
|
|
93
|
+
|
|
94
|
+
## Usage
|
|
95
|
+
|
|
96
|
+
To use this layer in your Nuxt project:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// nuxt.config.ts
|
|
100
|
+
export default defineNuxtConfig({
|
|
101
|
+
extends: [
|
|
102
|
+
'@cooperco/nuxt-layer-base'
|
|
103
|
+
]
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Nuxt 4 directory structure
|
|
108
|
+
This layer follows Nuxt 4's app directory structure:
|
|
109
|
+
- app/plugins/... for plugins (e.g., app/plugins/loggly.client.ts)
|
|
110
|
+
- app/composables/... for composables (e.g., app/composables/useLoggly.ts)
|
|
111
|
+
|
|
112
|
+
If you previously looked under plugins/ or composables/ at the layer root, note they have been relocated under app/ for Nuxt 4.
|
|
113
|
+
|
|
114
|
+
## Error logging (Loggly)
|
|
115
|
+
|
|
116
|
+
This base layer includes optional, secure error logging via Loggly using a server proxy. It is off by default and can be enabled per consuming app using environment variables.
|
|
117
|
+
|
|
118
|
+
What you get when enabled:
|
|
119
|
+
- Automatic server-side error capture (Nitro request errors)
|
|
120
|
+
- Automatic client-side error capture (Nuxt/Vue/global browser errors)
|
|
121
|
+
- Composable for custom logs from your features (useLoggly)
|
|
122
|
+
- Centralized PII scrubbing on the server proxy before forwarding to Loggly
|
|
123
|
+
|
|
124
|
+
### 1) Enable in your app (env vars)
|
|
125
|
+
Set the following environment variables in your consuming app (do not expose the token on the client):
|
|
126
|
+
|
|
127
|
+
- LOGGLY_ENABLED=true
|
|
128
|
+
- LOGGLY_TOKEN=your-loggly-customer-token
|
|
129
|
+
- LOGGLY_TAGS=nuxt,base,app-name,env (comma-separated; optional)
|
|
130
|
+
- LOG_LEVEL=error (optional; error|warn|info|debug)
|
|
131
|
+
- LOGGLY_ENDPOINT=https://logs-01.loggly.com/inputs (optional override)
|
|
132
|
+
|
|
133
|
+
Notes
|
|
134
|
+
- LOGGLY_TOKEN is server-only and never shipped to the client. The client sends logs to the server proxy at /api/loggly.
|
|
135
|
+
- If LOGGLY_ENABLED is not true or the token is missing, logging is skipped.
|
|
136
|
+
|
|
137
|
+
### 2) Using the composable in your app
|
|
138
|
+
The base layer exposes a composable you can call anywhere in your app.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
```ts
|
|
142
|
+
// inside a component or any composable
|
|
143
|
+
const log = useLoggly()
|
|
144
|
+
await log.error('Checkout failed', { orderId, step: 'place-order' }, ['checkout'])
|
|
145
|
+
await log.info('User clicked CTA', { campaign: 'summer' }, ['marketing'])
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
API:
|
|
149
|
+
- error(message, meta?, tags?)
|
|
150
|
+
- warn(message, meta?, tags?)
|
|
151
|
+
- info(message, meta?, tags?)
|
|
152
|
+
- debug(message, meta?, tags?)
|
|
153
|
+
|
|
154
|
+
Tags are merged with your global LOGGLY_TAGS (duplicates removed, limited to 10).
|
|
155
|
+
|
|
156
|
+
### 3) Auto-captured errors
|
|
157
|
+
When enabled, the base layer automatically forwards:
|
|
158
|
+
- Server: request-time exceptions via a Nitro plugin
|
|
159
|
+
- Client: Nuxt app errors, Vue component errors, window error events, and unhandled promise rejections
|
|
160
|
+
|
|
161
|
+
These are forwarded to /api/loggly, which safely shapes the payload before sending to Loggly.
|
|
162
|
+
|
|
163
|
+
### 4) Security and privacy
|
|
164
|
+
- The server proxy masks common sensitive keys (password, token, authorization, etc.) in meta payloads.
|
|
165
|
+
- Prefer adding any app-specific sensitive keys to your meta scrubbing in the proxy if needed.
|
|
166
|
+
- Logging is fire-and-forget; failures to log do not block requests or UI.
|
|
167
|
+
|
|
168
|
+
### 5) Verifying locally
|
|
169
|
+
1) In your consuming app, set env vars (e.g. in .env.local):
|
|
170
|
+
```
|
|
171
|
+
LOGGLY_ENABLED=true
|
|
172
|
+
LOGGLY_TOKEN=...your token...
|
|
173
|
+
LOGGLY_TAGS=nuxt,base,local,app-name
|
|
174
|
+
LOG_LEVEL=error
|
|
175
|
+
```
|
|
176
|
+
2) Start your app and induce an error (e.g., throw new Error('Test') in onMounted of a test component).
|
|
177
|
+
3) Watch your network tab for POST /api/loggly (200 OK). Then check Loggly for the entry with your tags.
|
|
178
|
+
|
|
179
|
+
### 6) Troubleshooting
|
|
180
|
+
- No logs? Ensure LOGGLY_ENABLED=true and LOGGLY_TOKEN is set in the server environment.
|
|
181
|
+
- CORS or network errors from the browser? The client never talks to Loggly directly; it only posts to /api/loggly.
|
|
182
|
+
- Too much volume? Consider adding sampling or batching later; the proxy makes this easy to change centrally.
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
## Playground: Test Loggly in the base layer
|
|
186
|
+
|
|
187
|
+
A tiny playground app is included so you can run the base layer by itself and verify Loggly end‑to‑end before integrating into another app.
|
|
188
|
+
|
|
189
|
+
Location: playground
|
|
190
|
+
|
|
191
|
+
How it works:
|
|
192
|
+
- The playground extends the base layer (extends: ['../layers/base']).
|
|
193
|
+
- It includes a simple page to send logs via useLoggly and buttons to trigger client/server errors.
|
|
194
|
+
- The server exposes GET /api/boom to intentionally throw and test server‑side logging.
|
|
195
|
+
|
|
196
|
+
Run it:
|
|
197
|
+
1) From the repo root:
|
|
198
|
+
- cd playground && npm run dev
|
|
199
|
+
|
|
200
|
+
2) Provide environment variables for Loggly in the playground (optional but required to actually send to Loggly):
|
|
201
|
+
Copy playground/.env.example to playground/.env and fill in your values.
|
|
202
|
+
|
|
203
|
+
LOGGLY_ENABLED=true
|
|
204
|
+
LOGGLY_TOKEN=your-loggly-customer-token
|
|
205
|
+
LOGGLY_TAGS=nuxt,base,playground,local
|
|
206
|
+
LOG_LEVEL=debug
|
|
207
|
+
# Optional override
|
|
208
|
+
# LOGGLY_ENDPOINT=https://logs-01.loggly.com/inputs
|
|
209
|
+
|
|
210
|
+
3) Open http://localhost:3000
|
|
211
|
+
- Click the Send error/warn/info/debug buttons to send custom logs.
|
|
212
|
+
- Click Throw client error to exercise the client plugin.
|
|
213
|
+
- Click Call /api/boom to exercise the server plugin.
|
|
214
|
+
|
|
215
|
+
Notes:
|
|
216
|
+
- If LOGGLY_ENABLED is not true or LOGGLY_TOKEN is missing, the server proxy will skip sending and respond with { ok: false, skipped: true }.
|
|
217
|
+
- The token is server‑only; it is never exposed to the client. The browser only talks to /api/loggly on your dev server.
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Publishing to npm (tag-based)
|
|
223
|
+
|
|
224
|
+
This layer is published using GitHub Actions when you push a tag that matches the base-vX.Y.Z pattern.
|
|
225
|
+
|
|
226
|
+
High-level flow:
|
|
227
|
+
- Bump the version in layers/base/package.json (SemVer).
|
|
228
|
+
- Commit and push your changes to main (or ensure the commit is on main).
|
|
229
|
+
- Create and push a tag named base-vX.Y.Z (matching the package.json version).
|
|
230
|
+
- The Publish Base Layer workflow installs deps, checks if that exact version already exists on npm, and if not, publishes privately with --access restricted.
|
|
231
|
+
|
|
232
|
+
Important notes:
|
|
233
|
+
- Do NOT rely on npm version creating a tag for you (it will create vX.Y.Z). We use a custom tag prefix base-v.
|
|
234
|
+
- First-time private publish is already configured via publishConfig.access: "restricted".
|
|
235
|
+
- The workflow will skip if the version already exists (you'll see "Skipping; version already exists.").
|
|
236
|
+
|
|
237
|
+
### Step-by-step
|
|
238
|
+
|
|
239
|
+
1) Bump the version in layers/base/package.json
|
|
240
|
+
- Option A (recommended): use npm version without creating a tag
|
|
241
|
+
- Bash:
|
|
242
|
+
```bash
|
|
243
|
+
cd layers/base
|
|
244
|
+
npm version patch --no-git-tag-version # or minor | major
|
|
245
|
+
```
|
|
246
|
+
- PowerShell:
|
|
247
|
+
```powershell
|
|
248
|
+
Set-Location layers/base
|
|
249
|
+
npm version patch --no-git-tag-version # or minor | major
|
|
250
|
+
```
|
|
251
|
+
- Option B: manually edit the version field in layers/base/package.json (SemVer: MAJOR.MINOR.PATCH)
|
|
252
|
+
|
|
253
|
+
2) Commit and push the change (from repo root or layers/base)
|
|
254
|
+
```bash
|
|
255
|
+
git add layers/base/package.json
|
|
256
|
+
git commit -m "chore(base): bump version"
|
|
257
|
+
git push origin main
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
3) Create and push the tag using the new version
|
|
261
|
+
- Get the new version value:
|
|
262
|
+
- Bash:
|
|
263
|
+
```bash
|
|
264
|
+
cd layers/base
|
|
265
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
266
|
+
cd ../..
|
|
267
|
+
git tag "base-v$VERSION"
|
|
268
|
+
git push origin "base-v$VERSION"
|
|
269
|
+
```
|
|
270
|
+
- PowerShell:
|
|
271
|
+
```powershell
|
|
272
|
+
Set-Location layers/base
|
|
273
|
+
$version = node -p "require('./package.json').version"
|
|
274
|
+
Set-Location ../..
|
|
275
|
+
git tag "base-v$version"
|
|
276
|
+
git push origin "base-v$version"
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
4) GitHub Actions will publish
|
|
280
|
+
- Workflow: .github/workflows/publish-base.yml
|
|
281
|
+
- Auth: uses NPM_TOKEN (Classic Automation token) configured as a GitHub secret
|
|
282
|
+
- Behavior: installs, checks npm view @cooperco/nuxt-layer-base@<version>, publishes if not found
|
|
283
|
+
|
|
284
|
+
### Troubleshooting
|
|
285
|
+
- Version already exists: bump the version again (patch/minor/major) and push a new tag.
|
|
286
|
+
- Auth errors (401/403): ensure NPM_TOKEN is set in repo secrets and org access is correct.
|
|
287
|
+
- Registry mismatch: this repo’s .npmrc pins @cooperco to https://registry.npmjs.org/; keep that file intact.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type LogLevel = 'error' | 'warn' | 'info' | 'debug'
|
|
2
|
+
|
|
3
|
+
export const useLoggly = () => {
|
|
4
|
+
const { public: pub } = useRuntimeConfig()
|
|
5
|
+
const enabled = !!pub?.logglyEnabled
|
|
6
|
+
const baseTags = (pub?.logglyTags || []) as string[]
|
|
7
|
+
|
|
8
|
+
const send = (
|
|
9
|
+
level: LogLevel,
|
|
10
|
+
message: string,
|
|
11
|
+
meta?: Record<string, unknown>,
|
|
12
|
+
tags: string[] = []
|
|
13
|
+
) => {
|
|
14
|
+
if (!enabled) return
|
|
15
|
+
const mergedTags = [...new Set([...baseTags, ...tags])].slice(0, 10)
|
|
16
|
+
return $fetch('/api/loggly', {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
body: { level, message, meta, tags: mergedTags }
|
|
19
|
+
}).catch(() => {})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
error: (m: string, meta?: Record<string, unknown>, tags?: string[]) => send('error', m, meta, tags),
|
|
24
|
+
warn: (m: string, meta?: Record<string, unknown>, tags?: string[]) => send('warn', m, meta, tags),
|
|
25
|
+
info: (m: string, meta?: Record<string, unknown>, tags?: string[]) => send('info', m, meta, tags),
|
|
26
|
+
debug: (m: string, meta?: Record<string, unknown>, tags?: string[]) => send('debug', m, meta, tags)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
2
|
+
const config = useRuntimeConfig()
|
|
3
|
+
if (!config.public?.logglyEnabled) return
|
|
4
|
+
|
|
5
|
+
const post = (payload: Record<string, unknown>) =>
|
|
6
|
+
$fetch('/api/loggly', { method: 'POST', body: payload }).catch(() => { /* swallow */ })
|
|
7
|
+
|
|
8
|
+
// Nuxt app-level errors
|
|
9
|
+
nuxtApp.hook('app:error', (err) => {
|
|
10
|
+
post({
|
|
11
|
+
level: 'error',
|
|
12
|
+
message: extractMessage(err) || 'App error',
|
|
13
|
+
error: toErrorShape(err),
|
|
14
|
+
tags: ['client', 'nuxt']
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// Vue component-level errors
|
|
19
|
+
const originalHandler = nuxtApp.vueApp.config.errorHandler
|
|
20
|
+
nuxtApp.vueApp.config.errorHandler = (err, instance, info) => {
|
|
21
|
+
post({
|
|
22
|
+
level: 'error',
|
|
23
|
+
message: extractMessage(err) || 'Vue error',
|
|
24
|
+
error: toErrorShape(err),
|
|
25
|
+
meta: { info, component: getComponentName(instance) },
|
|
26
|
+
tags: ['client', 'vue']
|
|
27
|
+
})
|
|
28
|
+
if (originalHandler) originalHandler(err, instance, info)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (import.meta.client) {
|
|
32
|
+
window.addEventListener('error', (event: ErrorEvent) => {
|
|
33
|
+
post({ level: 'error', message: event.message, error: toErrorShape(event.error), tags: ['client', 'window'] })
|
|
34
|
+
})
|
|
35
|
+
window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
|
|
36
|
+
const reason = event.reason
|
|
37
|
+
post({ level: 'error', message: extractMessage(reason) || 'Unhandled rejection', error: toErrorShape(reason), tags: ['client', 'promise'] })
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const toErrorShape = (err: unknown) => {
|
|
43
|
+
if (!err || typeof err !== 'object') return undefined
|
|
44
|
+
const e = err as { name?: string, message?: unknown, stack?: unknown }
|
|
45
|
+
return {
|
|
46
|
+
name: e.name || 'Error',
|
|
47
|
+
message: String(e.message ?? 'Error'),
|
|
48
|
+
stack: typeof e.stack === 'string' ? e.stack.split('\n').slice(0, 20) : undefined
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const extractMessage = (err: unknown): string | undefined => {
|
|
53
|
+
if (!err) return undefined
|
|
54
|
+
if (typeof err === 'string') return err
|
|
55
|
+
if (typeof err === 'object' && 'message' in err) {
|
|
56
|
+
return String((err as { message?: unknown }).message)
|
|
57
|
+
}
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const getComponentName = (instance: unknown): string | undefined => {
|
|
62
|
+
if (!instance) return undefined
|
|
63
|
+
const rec = instance as Record<string, unknown>
|
|
64
|
+
const typeVal = rec?.type as unknown
|
|
65
|
+
if (typeof typeVal === 'string') return typeVal
|
|
66
|
+
if (typeVal && typeof typeVal === 'object' && 'name' in typeVal) {
|
|
67
|
+
return String((typeVal as { name?: unknown }).name)
|
|
68
|
+
}
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
package/nuxt.config.ts
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
+
/* eslint-disable nuxt/nuxt-config-keys-order */
|
|
1
2
|
export default defineNuxtConfig({
|
|
2
|
-
modules: ['@nuxt/eslint', '@
|
|
3
|
+
modules: ['@nuxt/eslint', '@nuxtjs/i18n'],
|
|
3
4
|
devtools: { enabled: true },
|
|
4
5
|
compatibilityDate: '2025-07-15',
|
|
5
6
|
|
|
7
|
+
runtimeConfig: {
|
|
8
|
+
// server-only
|
|
9
|
+
logglyToken: process.env.LOGGLY_TOKEN || '',
|
|
10
|
+
logglyEndpoint: process.env.LOGGLY_ENDPOINT || 'https://logs-01.loggly.com/inputs',
|
|
11
|
+
|
|
12
|
+
// public (safe to expose)
|
|
13
|
+
public: {
|
|
14
|
+
// Allow either LOGGLY_ENABLED or NUXT_PUBLIC_LOGGLY_ENABLED to enable client features
|
|
15
|
+
logglyEnabled: (process.env.NUXT_PUBLIC_LOGGLY_ENABLED ?? process.env.LOGGLY_ENABLED) === 'true',
|
|
16
|
+
logglyTags: (process.env.NUXT_PUBLIC_LOGGLY_TAGS ?? process.env.LOGGLY_TAGS ?? 'nuxt,base').split(',').map(s => s.trim()).filter(Boolean),
|
|
17
|
+
logLevel: (process.env.NUXT_PUBLIC_LOG_LEVEL ?? process.env.LOG_LEVEL) || 'error' // error|warn|info|debug
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
6
21
|
typescript: {
|
|
7
22
|
tsConfig: {
|
|
8
23
|
compilerOptions: {
|
|
@@ -13,7 +28,14 @@ export default defineNuxtConfig({
|
|
|
13
28
|
|
|
14
29
|
eslint: {
|
|
15
30
|
config: {
|
|
16
|
-
stylistic: true
|
|
31
|
+
stylistic: true
|
|
17
32
|
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
i18n: {
|
|
36
|
+
locales: [
|
|
37
|
+
{ code: 'enUS', language: 'en-US' }
|
|
38
|
+
],
|
|
39
|
+
defaultLocale: 'enUS'
|
|
18
40
|
}
|
|
19
41
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cooperco/nuxt-layer-base",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"description": "Base Nuxt layer for cooperco projects",
|
|
13
13
|
"publishConfig": {
|
|
14
|
-
"access": "public"
|
|
14
|
+
"access": "public",
|
|
15
|
+
"registry": "https://registry.npmjs.org/"
|
|
15
16
|
},
|
|
16
17
|
"repository": {
|
|
17
18
|
"type": "git",
|
|
@@ -21,10 +22,13 @@
|
|
|
21
22
|
"author": "cooperco",
|
|
22
23
|
"license": "MIT",
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@nuxt/eslint": "^1.
|
|
25
|
+
"@nuxt/eslint": "^1.9.0",
|
|
26
|
+
"@nuxtjs/i18n": "^10.0.6",
|
|
27
|
+
"nuxt": "^4.0.3"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
25
30
|
"@nuxt/test-utils": "^3.19.2",
|
|
26
|
-
"eslint": "^9.
|
|
27
|
-
"
|
|
28
|
-
"vue-tsc": "^3.0.4"
|
|
31
|
+
"eslint": "^9.33.0",
|
|
32
|
+
"vue-tsc": "^3.0.5"
|
|
29
33
|
}
|
|
30
34
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineEventHandler, readBody } from 'h3'
|
|
2
|
+
|
|
3
|
+
type LogglyBody = {
|
|
4
|
+
level?: string
|
|
5
|
+
message?: string
|
|
6
|
+
error?: unknown
|
|
7
|
+
tags?: string[]
|
|
8
|
+
meta?: Record<string, unknown>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const sanitizeMeta = (input: unknown) => {
|
|
12
|
+
if (!input || typeof input !== 'object') return undefined
|
|
13
|
+
const blocked = ['password', 'token', 'authorization', 'auth', 'ssn', 'creditcard', 'credit_card']
|
|
14
|
+
const out: Record<string, unknown> = {}
|
|
15
|
+
for (const [k, v] of Object.entries(input as Record<string, unknown>)) {
|
|
16
|
+
out[k] = blocked.includes(k.toLowerCase()) ? '[REDACTED]' : v
|
|
17
|
+
}
|
|
18
|
+
return out
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const serializeError = (err: unknown) => {
|
|
22
|
+
if (!err || typeof err !== 'object') return undefined
|
|
23
|
+
const e = err as { name?: string, message?: unknown, stack?: unknown }
|
|
24
|
+
return {
|
|
25
|
+
name: e.name || 'Error',
|
|
26
|
+
message: String(e.message ?? 'Error'),
|
|
27
|
+
stack: typeof e.stack === 'string' ? e.stack.split('\n').slice(0, 20) : undefined
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default defineEventHandler(async (event) => {
|
|
32
|
+
const { logglyToken, logglyEndpoint, public: pub } = useRuntimeConfig()
|
|
33
|
+
if (!pub?.logglyEnabled || !logglyToken) {
|
|
34
|
+
return { ok: false, skipped: true }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const body = await readBody<LogglyBody>(event)
|
|
38
|
+
|
|
39
|
+
const scrubbed = {
|
|
40
|
+
level: body?.level ?? 'error',
|
|
41
|
+
message: body?.message?.toString().slice(0, 5000),
|
|
42
|
+
tags: [...new Set([...(pub.logglyTags || []), ...(body?.tags || [])])].slice(0, 10),
|
|
43
|
+
meta: sanitizeMeta(body?.meta),
|
|
44
|
+
error: body?.error ? serializeError(body.error) : undefined,
|
|
45
|
+
ts: new Date().toISOString()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const url = `${logglyEndpoint}/${encodeURIComponent(logglyToken)}/tag/${encodeURIComponent(scrubbed.tags.join(','))}`
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await $fetch(url, { method: 'POST', body: scrubbed })
|
|
52
|
+
return { ok: true }
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// do not throw here to avoid cascading failures
|
|
56
|
+
return { ok: false }
|
|
57
|
+
}
|
|
58
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
type NitroErrorCtx = {
|
|
2
|
+
event?: {
|
|
3
|
+
path?: string
|
|
4
|
+
method?: string
|
|
5
|
+
node?: { res?: { statusCode?: number } }
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const toErrorShape = (err: unknown) => {
|
|
10
|
+
if (!err || typeof err !== 'object') {
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
const e = err as { name?: string, message?: unknown, stack?: unknown }
|
|
14
|
+
return {
|
|
15
|
+
name: e.name || 'Error',
|
|
16
|
+
message: String(e.message ?? 'Error'),
|
|
17
|
+
stack: typeof e.stack === 'string' ? e.stack.split('\n').slice(0, 20) : undefined
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
22
|
+
const config = useRuntimeConfig()
|
|
23
|
+
if (!config.public?.logglyEnabled || !config.logglyToken) return
|
|
24
|
+
|
|
25
|
+
nitroApp.hooks.hook('error', async (error: unknown, ctx?: NitroErrorCtx) => {
|
|
26
|
+
try {
|
|
27
|
+
await $fetch('/api/loggly', {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
body: {
|
|
30
|
+
level: 'error',
|
|
31
|
+
message: (typeof error === 'object' && error && 'message' in error) ? String((error as { message?: unknown }).message) : 'Server error',
|
|
32
|
+
error: toErrorShape(error),
|
|
33
|
+
meta: {
|
|
34
|
+
url: ctx?.event?.path,
|
|
35
|
+
method: ctx?.event?.method,
|
|
36
|
+
status: ctx?.event?.node?.res?.statusCode
|
|
37
|
+
},
|
|
38
|
+
tags: ['server']
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// swallow logging errors
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
})
|