-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Filter events & analytics by metadata #2648
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
Changes from all commits
e8996e9
fea11d2
fd9f27c
2d5934a
9aefa03
5a36767
127ca9c
1c26a43
a2287c2
8ce567b
8c69b57
d6c229b
9e7bd00
76d5670
01dab20
a244e8a
284925f
56de4b2
b99c72d
200e1c3
ad5aede
4d76c88
090552a
ccaa34f
cfed824
4dd148f
cfdc5cf
a240dc5
1bb0850
c9ee2b9
d6ece15
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 |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| import { EventsFilters } from "./types"; | ||
|
|
||
| interface InternalFilter { | ||
| operand: string; | ||
| operator: | ||
| | "equals" | ||
| | "notEquals" | ||
| | "greaterThan" | ||
| | "lessThan" | ||
| | "greaterThanOrEqual" | ||
| | "lessThanOrEqual"; | ||
| value: string; | ||
| } | ||
|
|
||
| // Query parser that can parse the query string into a list of filters | ||
| export const queryParser = ( | ||
| query: EventsFilters["query"], | ||
| allowedOperands = ["metadata"], | ||
| ) => { | ||
| if (!query) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const filters: InternalFilter[] = []; | ||
|
|
||
| // Split the query by logical operators (AND/OR) to handle multiple conditions | ||
| // For now, we'll focus on single conditions, but this structure allows for future expansion | ||
| const conditions = query.split(/\s+(?:AND|and|OR|or)\s+/); | ||
|
|
||
| for (const condition of conditions) { | ||
| const trimmedCondition = condition.trim(); | ||
|
|
||
| if (!trimmedCondition) { | ||
| continue; | ||
| } | ||
|
|
||
| const filter = parseCondition(trimmedCondition); | ||
|
|
||
| if (!filter) { | ||
| continue; | ||
| } | ||
|
|
||
| const isAllowed = allowedOperands.some((allowed) => { | ||
| if (filter.operand === allowed) { | ||
| return true; | ||
| } | ||
|
|
||
| if (filter.operand.startsWith(`${allowed}.`)) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| }); | ||
|
|
||
| if (!isAllowed) { | ||
| continue; | ||
| } | ||
|
|
||
| filters.push(filter); | ||
| } | ||
|
|
||
| return filters.length > 0 ? filters : undefined; | ||
| }; | ||
|
|
||
| // Parses a single condition in the format: field:value, field>value, or metadata['key']:value | ||
| function parseCondition(condition: string): InternalFilter | null { | ||
| // This regex captures: | ||
| // 1. field - either a regular field name OR metadata with bracket notation (supports both single and double quotes) | ||
| // 2. operator - :, >, <, >=, <=, != | ||
| // 3. value - the value after the operator (supports quoted and unquoted values) | ||
| const unifiedPattern = | ||
| /^([a-zA-Z_][a-zA-Z0-9_]*|metadata\[['"][^'"]*['"]\](?:\[['"][^'"]*['"]\])*)\s*([:><=!]+)\s*(.+)$/; | ||
|
|
||
| const match = condition.match(unifiedPattern); | ||
|
|
||
| if (!match) { | ||
| return null; | ||
| } | ||
|
|
||
| // Extract the matched groups | ||
| const [, fieldOrMetadata, operator, value] = match; | ||
|
|
||
| let operand: string; | ||
|
|
||
| // Determine the operand based on whether it's metadata or a regular field | ||
| if (fieldOrMetadata.startsWith("metadata")) { | ||
| const keyPath = fieldOrMetadata.replace(/^metadata/, ""); | ||
|
|
||
| const extractedKey = keyPath | ||
| .replace(/^\[['"]|['"]\]$/g, "") // Remove leading [' or [" and trailing '] or "] | ||
| .replace(/\[['"]/g, ".") // Replace [' or [" with . | ||
| .replace(/['"]\]/g, ""); // Remove trailing '] or "] | ||
|
|
||
| operand = `metadata.${extractedKey}`; | ||
|
Comment on lines
+89
to
+94
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. Potential security risk with bracket notation parsing. The bracket notation parsing logic uses multiple regex replacements that could be vulnerable to malformed input. Consider using a more robust parsing approach or adding input validation to prevent potential issues. Apply this diff to add input validation: if (fieldOrMetadata.startsWith("metadata")) {
+ // Validate bracket notation format
+ if (!/^metadata(\[['"][^'"]*['"]\])+$/.test(fieldOrMetadata)) {
+ return null;
+ }
const keyPath = fieldOrMetadata.replace(/^metadata/, "");
🤖 Prompt for AI Agents |
||
| } else { | ||
| operand = fieldOrMetadata; | ||
| } | ||
|
|
||
| return { | ||
| operand, | ||
| operator: mapOperator(operator), | ||
| value: value.trim().replace(/^['"`]|['"`]$/g, ""), | ||
| }; | ||
| } | ||
|
|
||
| // Maps operator strings to our internal operator types | ||
| function mapOperator(operator: string): InternalFilter["operator"] { | ||
| switch (operator) { | ||
| case ":": | ||
| case "=": | ||
| return "equals"; | ||
| case ">": | ||
| return "greaterThan"; | ||
| case "<": | ||
| return "lessThan"; | ||
| case ">=": | ||
| return "greaterThanOrEqual"; | ||
| case "<=": | ||
| return "lessThanOrEqual"; | ||
| case "!=": | ||
| return "notEquals"; | ||
| default: | ||
| // For unsupported operators, default to equals | ||
| return "equals"; | ||
| } | ||
steven-tey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.