-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(plugin-server): Remove Postgres-based plugin error logging in favor of existing ClickHouse-based approaches #18764
Changes from all commits
b9c9336
c4cbd65
b71ea99
8d2a31a
82f4f16
8068ec3
684c764
29f5f99
182130c
ff5ce59
8f28d38
b30bb45
92ab3e8
b229260
7e3d94d
185d834
afbe6b7
ff4d6e9
9124858
e96da9d
b355cfe
4156eba
2a54885
3392e15
b6a4107
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
import { v4 as uuid4 } from 'uuid' | ||
|
||
import { ONE_HOUR } from '../src/config/constants' | ||
import { PluginLogEntryType } from '../src/types' | ||
import { UUIDT } from '../src/utils/utils' | ||
import { getCacheKey } from '../src/worker/vm/extensions/cache' | ||
import { | ||
|
@@ -13,7 +14,9 @@ import { | |
createTeam, | ||
enablePluginConfig, | ||
fetchEvents, | ||
fetchPluginAppMetrics, | ||
fetchPluginConsoleLogEntries, | ||
fetchPluginLogEntries, | ||
fetchPostgresPersons, | ||
getPluginConfig, | ||
redis, | ||
|
@@ -90,133 +93,119 @@ test.concurrent(`plugin method tests: event captured, processed, ingested`, asyn | |
}) | ||
}) | ||
|
||
test.concurrent(`plugin method tests: creates error on unhandled throw`, async () => { | ||
const plugin = await createPlugin({ | ||
organization_id: organizationId, | ||
name: 'test plugin', | ||
plugin_type: 'source', | ||
is_global: false, | ||
source__index_ts: ` | ||
test.concurrent( | ||
`plugin method tests: records error in app metrics and creates log entry on unhandled throw`, | ||
async () => { | ||
const plugin = await createPlugin({ | ||
organization_id: organizationId, | ||
name: 'test plugin', | ||
plugin_type: 'source', | ||
is_global: false, | ||
source__index_ts: ` | ||
export async function processEvent(event) { | ||
throw new Error('error thrown in plugin') | ||
} | ||
`, | ||
}) | ||
const teamId = await createTeam(organizationId) | ||
const pluginConfig = await createAndReloadPluginConfig(teamId, plugin.id) | ||
|
||
const distinctId = new UUIDT().toString() | ||
const uuid = new UUIDT().toString() | ||
}) | ||
const teamId = await createTeam(organizationId) | ||
const pluginConfig = await createAndReloadPluginConfig(teamId, plugin.id) | ||
|
||
const event = { | ||
event: 'custom event', | ||
// NOTE: Before `sanitizeJsonbValue` was added, the null byte below would blow up the error | ||
// UPDATE, breaking this test. It is now replaced with the Unicode replacement character, | ||
// \uFFFD. | ||
properties: { name: 'haha', other: '\u0000' }, | ||
} | ||
const distinctId = new UUIDT().toString() | ||
const uuid = new UUIDT().toString() | ||
|
||
await capture({ teamId, distinctId, uuid, event: event.event, properties: event.properties }) | ||
const event = { | ||
event: 'custom event', | ||
properties: { name: 'haha', other: '\u0000' }, | ||
} | ||
|
||
await waitForExpect(async () => { | ||
const events = await fetchEvents(teamId) | ||
expect(events.length).toBe(1) | ||
return events | ||
}) | ||
await capture({ teamId, distinctId, uuid, event: event.event, properties: event.properties }) | ||
|
||
const error = await waitForExpect(async () => { | ||
const pluginConfigAgain = await getPluginConfig(teamId, pluginConfig.id) | ||
expect(pluginConfigAgain.error).not.toBeNull() | ||
return pluginConfigAgain.error | ||
}) | ||
await waitForExpect(async () => { | ||
const events = await fetchEvents(teamId) | ||
expect(events.length).toBe(1) | ||
return events | ||
}) | ||
|
||
expect(error.message).toEqual('error thrown in plugin') | ||
const errorProperties = error.event.properties | ||
expect(errorProperties.name).toEqual('haha') | ||
expect(errorProperties.other).toEqual('\uFFFD') | ||
}) | ||
const appMetric = await waitForExpect(async () => { | ||
const errorMetrics = await fetchPluginAppMetrics(pluginConfig.id) | ||
expect(errorMetrics.length).toEqual(1) | ||
return errorMetrics[0] | ||
}) | ||
|
||
test.concurrent(`plugin method tests: creates error on unhandled rejection`, async () => { | ||
const plugin = await createPlugin({ | ||
organization_id: organizationId, | ||
name: 'test plugin', | ||
plugin_type: 'source', | ||
is_global: false, | ||
source__index_ts: ` | ||
export async function processEvent(event) { | ||
void new Promise((_, rejects) => { rejects(new Error('error thrown in plugin')) }).then(() => {}) | ||
return event | ||
} | ||
`, | ||
}) | ||
const teamId = await createTeam(organizationId) | ||
const pluginConfig = await createAndReloadPluginConfig(teamId, plugin.id) | ||
expect(appMetric.successes).toEqual(0) | ||
expect(appMetric.failures).toEqual(1) | ||
expect(appMetric.error_type).toEqual('Error') | ||
expect(JSON.parse(appMetric.error_details!)).toMatchObject({ | ||
error: { message: 'error thrown in plugin' }, | ||
event: { properties: event.properties }, | ||
}) | ||
Comment on lines
+129
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've learned to really like doing a single check, because then if the test fails I see directly what it was instead and it's quicker for me to know what broke, i.e.
Does this make sense? Not expecting you to change an already merged PR more for maybe let's do this in the future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, that makes sense! (I do need to spend some time getting more familiar with the different There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure my code there is valid btw - I let copilot write it for me normally 😅 |
||
|
||
const distinctId = new UUIDT().toString() | ||
const uuid = new UUIDT().toString() | ||
const errorLogEntry = await waitForExpect(async () => { | ||
const errorLogEntries = (await fetchPluginLogEntries(pluginConfig.id)).filter( | ||
(entry) => entry.type == PluginLogEntryType.Error | ||
) | ||
expect(errorLogEntries.length).toBe(1) | ||
return errorLogEntries[0] | ||
}) | ||
|
||
const event = { | ||
event: 'custom event', | ||
properties: { name: 'haha' }, | ||
expect(errorLogEntry.message).toContain('error thrown in plugin') | ||
} | ||
) | ||
|
||
await capture({ teamId, distinctId, uuid, event: event.event, properties: event.properties }) | ||
|
||
await waitForExpect(async () => { | ||
const events = await fetchEvents(teamId) | ||
expect(events.length).toBe(1) | ||
return events | ||
}) | ||
|
||
const error = await waitForExpect(async () => { | ||
const pluginConfigAgain = await getPluginConfig(teamId, pluginConfig.id) | ||
expect(pluginConfigAgain.error).not.toBeNull() | ||
return pluginConfigAgain.error | ||
}) | ||
|
||
expect(error.message).toEqual('error thrown in plugin') | ||
}) | ||
|
||
test.concurrent(`plugin method tests: creates error on unhandled promise errors`, async () => { | ||
const plugin = await createPlugin({ | ||
organization_id: organizationId, | ||
name: 'test plugin', | ||
plugin_type: 'source', | ||
is_global: false, | ||
source__index_ts: ` | ||
test.concurrent( | ||
`plugin method tests: records success in app metrics and creates error log entry on unawaited promise rejection`, | ||
async () => { | ||
const plugin = await createPlugin({ | ||
organization_id: organizationId, | ||
name: 'test plugin', | ||
plugin_type: 'source', | ||
is_global: false, | ||
source__index_ts: ` | ||
export async function processEvent(event) { | ||
void new Promise(() => { throw new Error('error thrown in plugin') }).then(() => {}) | ||
void new Promise(() => { throw new Error('error thrown in plugin') }) | ||
return event | ||
} | ||
`, | ||
}) | ||
const teamId = await createTeam(organizationId) | ||
const pluginConfig = await createAndReloadPluginConfig(teamId, plugin.id) | ||
}) | ||
const teamId = await createTeam(organizationId) | ||
const pluginConfig = await createAndReloadPluginConfig(teamId, plugin.id) | ||
|
||
const distinctId = new UUIDT().toString() | ||
const uuid = new UUIDT().toString() | ||
const distinctId = new UUIDT().toString() | ||
const uuid = new UUIDT().toString() | ||
|
||
const event = { | ||
event: 'custom event', | ||
properties: { name: 'haha' }, | ||
} | ||
const event = { | ||
event: 'custom event', | ||
properties: { name: 'haha' }, | ||
} | ||
|
||
await capture({ teamId, distinctId, uuid, event: event.event, properties: event.properties }) | ||
await capture({ teamId, distinctId, uuid, event: event.event, properties: event.properties }) | ||
|
||
await waitForExpect(async () => { | ||
const events = await fetchEvents(teamId) | ||
expect(events.length).toBe(1) | ||
return events | ||
}) | ||
await waitForExpect(async () => { | ||
const events = await fetchEvents(teamId) | ||
expect(events.length).toBe(1) | ||
return events | ||
}) | ||
|
||
const error = await waitForExpect(async () => { | ||
const pluginConfigAgain = await getPluginConfig(teamId, pluginConfig.id) | ||
expect(pluginConfigAgain.error).not.toBeNull() | ||
return pluginConfigAgain.error | ||
}) | ||
const appMetric = await waitForExpect(async () => { | ||
const appMetrics = await fetchPluginAppMetrics(pluginConfig.id) | ||
expect(appMetrics.length).toEqual(1) | ||
return appMetrics[0] | ||
}) | ||
|
||
expect(error.message).toEqual('error thrown in plugin') | ||
}) | ||
expect(appMetric.successes).toEqual(1) | ||
expect(appMetric.failures).toEqual(0) | ||
|
||
const errorLogEntry = await waitForExpect(async () => { | ||
const errorLogEntries = (await fetchPluginLogEntries(pluginConfig.id)).filter( | ||
(entry) => entry.type == PluginLogEntryType.Error | ||
) | ||
expect(errorLogEntries.length).toBe(1) | ||
return errorLogEntries[0] | ||
}) | ||
|
||
expect(errorLogEntry.message).toContain('error thrown in plugin') | ||
} | ||
Comment on lines
+189
to
+207
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a subtle distinction here that I hadn't originally considered until making this change: it's possible for a plugin execution to succeed (and be marked as such in app metrics), but also log an error at some later time in a case similar to this one where an un-awaited promise is later rejected. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All plugins will be non-async in the future ... that's my current plan at least. |
||
) | ||
|
||
test.concurrent(`plugin method tests: teardown is called on stateful plugin reload if they are updated`, async () => { | ||
const plugin = await createPlugin({ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding of the
Promise
API is that this test and the one below it ("plugin method tests: creates error on unhandled promise errors") are functionally identical, so I consolidated them into a single test.