-
Notifications
You must be signed in to change notification settings - Fork 43
Add track() API for custom event tracking + config updates via SSE #984
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
Open
hansott
wants to merge
52
commits into
main
Choose a base branch
from
add-track-api
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
a67d527
Add track() API for custom event tracking
hansott 4c173f9
Remove unused metadata field from TrackedEvent
hansott eb3912e
Send tracked events to the realtime endpoint
hansott cb5b883
Merge branch 'main' of github.com:AikidoSec/firewall-node into add-tr…
hansott 629a1a8
Fix user events endpoint and drop userAgent
hansott 994a802
Replace config polling with SSE streaming
hansott abbf9b6
Simplify SSE connection and remove unused cleanup
hansott 3498454
Add login route with track events to express-mysql sample app
hansott 6084a60
Add AIKIDO_DEBUG_SSE env var and remove polling fallback
hansott e91caf2
Clear existing timer before scheduling SSE reconnect
hansott dfafad2
Merge branch 'main' of github.com:AikidoSec/firewall-node into add-tr…
hansott 5f76bbb
Add config updated polling again
timokoessler 03c66ce
Merge branch 'main' of github.com:AikidoSec/firewall-node into add-tr…
hansott 07fe7c4
Merge branch 'main' of github.com:AikidoSec/firewall-node into add-tr…
hansott 713b86d
Use zen.aikido.dev as default realtime hostname
hansott e58b480
Use getRealtimeURL directly for SSE connections
hansott f279f5b
Add read timeout to SSE client
hansott 7c62ffd
Inline ConfigUpdateOptions into each consumer
hansott c36921c
Inline pollingURL variable
hansott d0e97c6
Retry polling URL probe with backoff
hansott 4d6aeb0
Simplify SSE reconnect backoff
hansott 692609e
Add SSE stream endpoint to mock server and e2e test
hansott 081846c
Add logDebug helper to SSE modules
hansott b4423cd
Skip SSE when realtime endpoint is unreachable
hansott 4c50247
Fix mock SSE event data to match zen-realtime
hansott 682918e
Add SSE reconnect e2e test
hansott 1227246
Log and ignore invalid SSE config-updated payloads
hansott 7703dea
Format code
hansott 86e3e74
Remove token from sink tests to avoid probe hostname leak
hansott 85cc33a
Drain probeRealtimeURL fetch timeout in Agent tests
hansott a88ae85
Allow localhost in outbound blocking e2e test
hansott abddb97
Account for SSE connection in heartbeat hostname hits
hansott 55f9c2a
Add jitter and stable-connection backoff to SSE reconnect
hansott b88692c
Stop SSE reconnect on 401 and 403
hansott 4397a76
Allow localhost in outbound blocking e2e test
hansott 8924af4
Add token to Fetch test to fix attack event assertion
hansott d4d0c9a
Account for probe hostname in Fetch and Undici tests
hansott 708666f
Sort hostnames in Undici test to fix ordering flake
hansott 6e49962
Restore token in HTTPRequest test for attack event assertion
hansott ed006ef
Clear probe hostname before sink test assertions
hansott 5ae3353
Wait for realtime probe before sink test assertions
hansott c6499bb
Merge branch 'main' into add-track-api
timokoessler 71839c9
Fix double backoff in SSE reconnect
hansott 905bc15
Add unit tests for connectToSSE
hansott 1d05922
Consolidate SSE unit tests into fewer files
hansott ea5c50c
Add unit test for SSE read timeout
hansott 29f937f
Reset backoff on SSE socket timeout
hansott e3656fa
Split connectToSSE into connect + reconnect loop
hansott 18fd832
Log SSE loop errors instead of swallowing them
hansott 768dfa8
Fix SSE test compat with Node 16 and Node 26
hansott 8520477
Avoid t.match with regex inside array in 401 test
hansott 16a288a
Use setTimeout from node:timers/promises
hansott File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # Tracking events | ||
|
|
||
| `track` lets you record things happening in your app — like failed logins, signups, or password resets. Zen sends these to Aikido so patterns can be detected, like someone failing to log in 50 times in a minute. | ||
|
|
||
| ```js | ||
| const Zen = require("@aikidosec/firewall"); | ||
|
|
||
| app.post("/login", async (req, res) => { | ||
| const user = await authenticate(req.body.username, req.body.password); | ||
|
|
||
| if (!user) { | ||
| Zen.track("user.login_failed"); | ||
| return res.status(401).json({ error: "Invalid credentials" }); | ||
| } | ||
|
|
||
| Zen.setUser({ id: user.id }); | ||
| Zen.track("user.login_succeeded"); | ||
| res.json({ token: createToken(user) }); | ||
| }); | ||
| ``` | ||
|
|
||
| Zen automatically picks up the IP address, user agent, and current user (if you called [`setUser`](./user.md)) from the request — you don't need to pass those yourself. | ||
|
|
||
| ## More examples | ||
|
|
||
| ```js | ||
| Zen.track("user.signed_up"); | ||
| Zen.track("user.password_reset_requested"); | ||
| Zen.track("plan.invite_sent"); | ||
| Zen.track("payment.failed"); | ||
| ``` | ||
|
|
||
| ## Naming events | ||
|
|
||
| Use lowercase with dots to group related events: | ||
|
|
||
| - `user.login_failed` | ||
| - `user.login_succeeded` | ||
| - `user.signed_up` | ||
| - `user.password_reset_requested` | ||
| - `payment.failed` | ||
| - `plan.invite_sent` | ||
|
|
||
| ## Things to know | ||
|
|
||
| `track` only works inside an HTTP request. If you call it in a background job or a script, nothing gets sent and you'll see a warning in the console. | ||
|
|
||
| If you haven't called `setUser` yet, the event still goes through — it just won't have a user ID attached. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { fetch } from "../../helpers/fetch"; | ||
| import { getRealtimeURL } from "../realtime/getRealtimeURL"; | ||
| import type { Token } from "./Token"; | ||
|
|
||
| export type UserEvent = { | ||
| name: string; | ||
| userId: string | undefined; | ||
| ipAddress: string | undefined; | ||
| userAgent: string | undefined; | ||
| }; | ||
|
|
||
| export async function sendUserEvent( | ||
| token: Token, | ||
| event: UserEvent | ||
| ): Promise<void> { | ||
| await fetch({ | ||
| url: new URL(`${getRealtimeURL().toString()}api/runtime/events/user`), | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| Authorization: token.asString(), | ||
| }, | ||
| body: JSON.stringify(event), | ||
| timeoutInMS: 5000, | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { getInstance } from "../AgentSingleton"; | ||
| import { ContextStorage } from "./ContextStorage"; | ||
|
|
||
| export function track(eventName: string): void { | ||
| const agent = getInstance(); | ||
|
|
||
| if (!agent) { | ||
| return; | ||
| } | ||
|
|
||
| if (typeof eventName !== "string" || eventName.length === 0) { | ||
| agent.log(`track(...) expects a non-empty string as event name.`); | ||
| return; | ||
| } | ||
|
|
||
| const context = ContextStorage.getStore(); | ||
| if (!context) { | ||
| logWarningTrackCalledWithoutContext(); | ||
| return; | ||
| } | ||
|
|
||
| const userAgent = | ||
| typeof context.headers["user-agent"] === "string" | ||
| ? context.headers["user-agent"] | ||
| : undefined; | ||
|
|
||
| agent.onTrackEvent({ | ||
| name: eventName, | ||
| userId: context.user?.id, | ||
| ipAddress: context.remoteAddress, | ||
| userAgent, | ||
| }); | ||
| } | ||
|
|
||
| let loggedWarningTrackCalledWithoutContext = false; | ||
|
|
||
| function logWarningTrackCalledWithoutContext() { | ||
| if (loggedWarningTrackCalledWithoutContext) { | ||
| return; | ||
| } | ||
|
|
||
| // oxlint-disable-next-line no-console | ||
| console.warn( | ||
| "track(...) was called without a context. The event will not be tracked. Make sure to call track(...) within an HTTP request." | ||
| ); | ||
|
|
||
| loggedWarningTrackCalledWithoutContext = true; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.