Structr
Expert Topics
Built-in Analytics
Structr includes a built-in analytics system that allows you to build custom analytics and audit functionality into your application. You can record events like page views, user actions, or business transactions, and later query them with filtering, aggregation, and time-based analysis.
This feature is similar to tools like Google Analytics, but runs entirely within your Structr instance. All data stays on your server, giving you full control over what you track and how you analyze it.
Overview
Event tracking consists of two parts:
- The
logEvent()function records events from your application code - The
/structr/rest/logendpoint queries and analyzes recorded events
Events are stored as LogEvent entities in the database with the following properties:
| Property | Description |
|---|---|
| action | The type of event (e.g., “VIEW”, “CLICK”, “PURCHASE”) |
| message | Additional details about the event |
| subject | Who triggered the event (typically a user ID) |
| object | What the event relates to (typically a content ID) |
| timestamp | When the event occurred (set automatically) |
Recording Events
You can record events in two ways: using the logEvent() function from your application code, or by posting directly to the REST endpoint.
Using logEvent()
StructrScript:
${logEvent('VIEW', 'User viewed article', me.id, article.id)}
JavaScript:
$.logEvent('VIEW', 'User viewed article', $.me.id, article.id);
The parameters are:
- action (required) – The event type
- message (required) – A description or additional data
- subject (optional) – Who triggered the event
- object (optional) – What the event relates to
JavaScript Object Syntax
In JavaScript, you can also pass a single object:
$.logEvent({
action: 'PURCHASE',
message: 'Order completed',
subject: $.me.id,
object: order.id
});
Using the REST API
You can also create events via POST request, which is useful for external systems or JavaScript frontends:
POST /structr/rest/log
Content-Type: application/json
{
"action": "VIEW",
"message": "User viewed article",
"subject": "user-uuid-here",
"object": "article-uuid-here"
}
When using the REST API, subject, object, and action are required. The timestamp is set automatically.
Common Patterns
Track page views in a page’s onRender method:
$.logEvent('VIEW', request.path, $.me?.id, thisPage.id);
Track user actions in event handlers:
$.logEvent('DOWNLOAD', file.name, $.me.id, file.id);
Track business events in lifecycle methods:
// In Order.afterCreate
$.logEvent('ORDER_CREATED', 'New order: ' + this.total, this.customer.id, this.id);
Querying Events
The /structr/rest/log endpoint provides flexible querying capabilities.
Query Parameters
| Parameter | Description |
|---|---|
subject | Filter by subject ID |
object | Filter by object ID |
action | Filter by action type |
timestamp | Filter by time range using [start TO end] syntax |
aggregate | Group by time using SimpleDateFormat pattern |
histogram | Extract and count values from messages using regex |
filters | Filter messages by regex patterns (separated by ::) |
multiplier | Extract numeric multiplier from message using regex |
correlate | Filter based on related events (see Correlation section) |
Overview Query
Without parameters, the endpoint returns a summary of all recorded events:
GET /structr/rest/log
Response:
{
"result": [{
"actions": "VIEW, CLICK, PURCHASE",
"entryCount": 15423,
"firstEntry": "2026-01-01T00:00:00+0000",
"lastEntry": "2026-02-03T14:30:00+0000"
}]
}
Filtering Events
Filter by subject, object, action, or time range:
GET /structr/rest/log?subject=<userId>
GET /structr/rest/log?object=<articleId>
GET /structr/rest/log?action=VIEW
GET /structr/rest/log?subject=<userId>&action=PURCHASE
Time Range Queries
Filter by timestamp using range syntax:
GET /structr/rest/log?timestamp=[2026-01-01T00:00:00+0000 TO 2026-01-31T23:59:59+0000]
Aggregation
The aggregate parameter groups events by time intervals. It accepts a Java SimpleDateFormat pattern that defines the grouping granularity:
| Pattern | Groups by |
|---|---|
yyyy | Year |
yyyy-MM | Month |
yyyy-MM-dd | Day |
yyyy-MM-dd HH | Hour |
yyyy-MM-dd HH:mm | Minute |
Example – count events per day:
GET /structr/rest/log?action=VIEW&aggregate=yyyy-MM-dd
You can add custom aggregation patterns as additional query parameters. Each pattern is a regex that matches against the message field:
GET /structr/rest/log?action=VIEW&aggregate=yyyy-MM-dd&category=category:(.*)&premium=premium:true
This groups by day and counts how many messages match each pattern. The response includes a total count plus counts for each named pattern.
Multiplier
When aggregating, you can extract a numeric value from the message to use as a multiplier instead of counting each event as 1:
GET /structr/rest/log?action=PURCHASE&aggregate=yyyy-MM-dd&multiplier=amount:(\d+)
If an event message contains amount:150, it contributes 150 to the total instead of 1. This is useful for summing values like order amounts or quantities.
Histograms
The histogram parameter extracts values from messages using a regex pattern with a capture group, creating a breakdown by those values:
GET /structr/rest/log?action=VIEW&aggregate=yyyy-MM-dd&histogram=category:(.*)
This returns counts grouped by both time (from aggregate) and by the captured category value. The response shows how many events occurred for each category in each time period.
Filters
The filters parameter applies regex patterns to the message field. Only events where all patterns match are included. Separate multiple patterns with :::
GET /structr/rest/log?action=VIEW&filters=premium:true::region:EU
This returns only VIEW events where the message contains both premium:true and region:EU.
Correlation
Correlation allows you to filter events based on the existence of related events. This is useful for questions like “show me all views of articles that were later purchased” or “find users who viewed but did not buy”.
The correlation parameter has the format:
correlate=ACTION::OPERATOR::PATTERN
The components are:
- ACTION – The action type to correlate with (e.g., “PURCHASE”)
- OPERATOR – How to match:
and,andSubject,andObject, ornot - PATTERN – A regex pattern to extract a correlation key from the message
Example: Find views that led to purchases
GET /structr/rest/log?action=VIEW&correlate=PURCHASE::and::article-(.*)
This returns VIEW events only if there is also a PURCHASE event where the pattern article-(.*) extracts the same value from the message.
Operators:
| Operator | Description |
|---|---|
and | Include event if a correlating event exists |
andSubject | Include event if a correlating event exists with the same subject |
andObject | Include event if a correlating event exists with the same object |
not | Include event only if NO correlating event exists |
Example: Find users who viewed but did not purchase
GET /structr/rest/log?action=VIEW&correlate=PURCHASE::not::article-(.*)
This is an advanced feature that requires careful design of your event messages to include matchable patterns.
Designing Event Messages
The power of the query features depends on how you structure your event messages. A well-designed message format makes filtering, aggregation, and correlation much easier.
Key-Value Format
A recommended pattern is to use key-value pairs in your messages:
$.logEvent('PURCHASE', 'category:electronics amount:299 region:EU premium:true', $.me.id, order.id);
This format allows you to:
- Filter by any attribute:
filters=premium:true - Extract values for histograms:
histogram=category:(.*?) - Sum amounts:
multiplier=amount:(\d+) - Correlate by category:
correlate=VIEW::and::category:(.*?)
JSON Format
For complex data, you can store JSON in the message:
$.logEvent('PURCHASE', JSON.stringify({
category: 'electronics',
amount: 299,
region: 'EU'
}), $.me.id, order.id);
Note that JSON is harder to query with regex patterns, but useful when you need to retrieve and parse the full event data later.
Consistent Naming
Use consistent action names across your application:
- Use uppercase for action types:
VIEW,CLICK,PURCHASE - Use a prefix for related actions:
FUNNEL_START,FUNNEL_STEP,FUNNEL_COMPLETE - Document your event schema so team members use the same format
Use Cases
Page View Analytics
Track which pages are most popular:
// In page onRender
$.logEvent('VIEW', thisPage.name, $.me?.id, thisPage.id);
Query most viewed pages:
GET /structr/rest/log?action=VIEW&aggregate=object
User Activity Tracking
Track what a specific user does:
GET /structr/rest/log?subject=<userId>
Conversion Funnels
Track steps in a process:
$.logEvent('FUNNEL_STEP', 'cart', $.me.id, session.id);
$.logEvent('FUNNEL_STEP', 'checkout', $.me.id, session.id);
$.logEvent('FUNNEL_STEP', 'payment', $.me.id, session.id);
$.logEvent('FUNNEL_STEP', 'complete', $.me.id, session.id);
Audit Trails
Track who changed what:
// In onSave lifecycle method
let mods = $.retrieve('modifications');
$.logEvent('MODIFIED', JSON.stringify(mods.after), $.me.id, this.id);
Performance Considerations
- LogEvent entities are regular database nodes and count toward your database size
- Consider implementing a retention policy to delete old events
- For high-traffic applications, consider batching events or using sampling
- Index the properties you filter on most frequently
Related Topics
- Monitoring – System-level monitoring and health checks
- Lifecycle Methods – Recording events in onCreate, onSave, onDelete
- Scheduled Tasks – Implementing retention policies or generating reports
Migration Guide
This chapter covers breaking changes and migration steps when upgrading between major Structr versions.
Important: Always create a full backup before upgrading Structr.
Migrating to Structr 6.x
Version 6 introduces several breaking changes that require manual migration from 5.x.
Global Schema Methods
Global schema methods have been simplified. The globalSchemaMethods namespace no longer exists – functions can now be called directly from the root context.
StructrScript / JavaScript:
// Old (5.x)
$.globalSchemaMethods.foo()
// New (6.x)
$.foo()
REST API:
# Old (5.x)
/structr/rest/maintenance/globalSchemaMethods/foo
# New (6.x)
/structr/rest/foo
Action required: Search your codebase for
/maintenance/globalSchemaMethodsand$.globalSchemaMethodsand update all occurrences.
REST API Query Parameter Change
The _loose parameter has been renamed to _inexact.
# Old (5.x)
/structr/rest/foo?_loose=1
# New (6.x)
/structr/rest/foo?_inexact=1
REST API Response Structure
The response body from $.GET and $.POST requests is now accessible via the body property.
// Old (5.x)
JSON.parse($.GET(url))
// New (6.x)
JSON.parse($.GET(url).body)
Schema Inheritance
The extendsClass property on schema nodes has been replaced with inheritedTraits.
// Old (5.x)
eq('Location', get(first(find('SchemaNode', 'name', request.type)), 'extendsClass').name)
// New (6.x)
contains(first(find('SchemaNode', 'name', request.type)).inheritedTraits, 'Location')
JavaScript Function Return Behavior
JavaScript functions now return their result directly by default.
Option 1: Restore old behavior globally:
application.scripting.js.wrapinmainfunction = true
Option 2: Remove unnecessary return statements from functions.
JavaScript Strict Mode
Identifiers must be declared before use. Assigning to undeclared variables throws a ReferenceError.
// ❌ Not allowed
foo = 1;
for (foo of array) {}
// ✅ Correct
let foo = 1;
for (let foo of array) {}
Custom Indices
Custom indices are dropped during the upgrade to 6.0.
Action required: Recreate all custom indices manually after upgrading.
Upload Servlet Changes
| Aspect | 5.x Behavior | 6.x Behavior |
|---|---|---|
| Default upload folder | Root or configurable | /._structr_uploads |
| Empty folder setting | Allowed | Enforced non-empty |
uploadFolderPath | Unrestricted | Authenticated users only |
Repeaters: No REST Queries
REST queries are no longer allowed for repeaters. Migrate them to function queries or flows.
Migration Checklist for 6.x
- [ ] Replace
$.globalSchemaMethods.xyz()with$.xyz() - [ ] Update REST URLs: remove
/maintenance/globalSchemaMethods/ - [ ] Replace
_loosewith_inexact - [ ] Update
$.GET/$.POSTcalls to use.body - [ ] Replace
extendsClasswithinheritedTraits - [ ] Review JavaScript functions for return statement compatibility
- [ ] Declare all JavaScript variables properly
- [ ] Recreate custom indices after upgrade
- [ ] Review upload handling code
- [ ] Migrate repeater REST queries to function queries
Migrating to Structr 4.x
All versions starting with the 4.0 release include breaking changes which require migration of applications built with Structr versions prior to 4.0 (1.x, 2.x and 3.x).
GraalVM Migration
With version 4.0, the required Java Runtime changed from standard JVMs (OpenJDK, Oracle JDK) to GraalVM. GraalVM brings full ECMAScript support, better performance, and polyglot scripting capabilities.
Installing GraalVM
Each Structr version supports the stable GraalVM version current at the time of release. The following example shows installation on Linux:
wget https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.1.0/graalvm-ce-java11-linux-amd64-22.1.0.tar.gz
tar xvzf graalvm-ce-java11-linux-amd64-22.1.0.tar.gz
sudo mv graalvm-ce-java11-22.1.0 /usr/lib/jvm
sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/graalvm-ce-java11-22.1.0/bin/java 2210
sudo update-alternatives --auto java
Migration of Script Expressions
Predicates in find() and search()
All predicates in find() and search() expressions need the $.predicate prefix. The easiest way to migrate is to export the application using deployment export and search all files for these predicates:
$.and
$.or
$.not
$.equals
$.contains
$.empty
$.range
$.within_distance
$.sort
$.page
Examples:
// Old (3.x)
$.find('File', 'size', $.range(null, 100), $.page(1, 10));
// New (4.x+)
$.find('File', 'size', $.predicate.range(null, 100), $.predicate.page(1, 10));
// Old (3.x)
$.find('User', $.sort('createdDate'));
// New (4.x+)
$.find('User', $.predicate.sort('createdDate'));
Some predicates also exist as regular functions ($.sort(), $.empty()) or keywords ($.page). When used outside of find(), they don’t need changes:
// No change needed - sort() used outside find()
$.sort($.find('User'), 'createdDate');
Resource Access Permissions
Resource Permissions (formerly “Resource Access Grants”) have been made more flexible. Rights management now also applies to permission nodes themselves, requiring users to have read access to the permission object to use it.
Manual Migration
- Log in as admin
- Navigate to Security → Resource Permissions
- Enable “Show only used grants”
- Migrate permissions:
- If the permission has active flags for “Public Users”: set
visibleToPublicUsers = true - If the permission has active flags for “Authenticated Users”: set
visibleToAuthenticatedUsers = true - If both flags apply: split into two permissions with identical signatures
For many permissions, enable “Show visibility flags in Resource Permissions table” in Dashboard → UI Settings.
Semi-automatic Migration via Deployment
When importing a deployment export from a pre-4.0 version into 4.x+, Structr runs automatic migration using this heuristic:
- Public Users flags →
visibleToPublicUsers = true - Authenticated Users flags →
visibleToAuthenticatedUsers = true - If both flags are set, a warning is issued to split the grant (since
visibleToPublicUsers = truealso makes the object visible to authenticated users)
Scripting Considerations
Date Comparisons
Use the getTime() function when comparing dates to avoid issues with GraalVM ProxyDate entities:
{
return $.me.createdDate.getTime() <= $.now.getTime();
}
Conditional Chaining Limitation
Conditional chaining on ProxyObjects with function members can cause errors:
{
const obj = {
method1: () => "works"
};
// Works
obj.method1?.();
// Works, call doesn't get executed
obj.method2?.();
const proxyObject = $.retrieve('passedObject');
// Does NOT work - throws unsupported message exception
proxyObject.myMethod?.();
}
REST Request Parameters
Starting with 4.0, REST request parameters must be prefixed with underscore to prevent name collisions with property names:
# Old
/structr/rest/Project?page=1&pageSize=10&sort=name
# New
/structr/rest/Project?_page=1&_pageSize=10&_sort=name
Full list of affected parameters:
| Parameter | Parameter | Parameter |
|---|---|---|
page | pageSize | sort |
order | loose | locale |
latlon | location | state |
house | country | postalCode |
city | street | distance |
outputNestingDepth | debugLoggingEnabled | forceResultCount |
disableSoftLimit | parallelizeJsonOutput | batchSize |
Legacy mode can be enabled with application.legacy.requestparameters.enabled = true but is discouraged for new projects.
Neo4j Upgrade
Neo4j 4.x is recommended for Structr 4.x, though Neo4j 3.5 is still supported. If upgrading Neo4j, consult the Neo4j changelog.
Cypher Parameter Syntax
The old parameter syntax {param} was deprecated in Neo4j 3.0 and removed in Neo4j 4.0. Use $param instead. For compatibility, you can prefix queries with CYPHER 3.5.
Database Name Configuration
If migrating from Neo4j versions prior to 4, the default database may be named graph.db instead of neo4j. Configure the database name in structr.conf:
YOUR_DB_NAME.database.connection.url = bolt://localhost:7687
YOUR_DB_NAME.database.connection.name = YOUR_DB_NAME
YOUR_DB_NAME.database.connection.password = your_neo4j_password
YOUR_DB_NAME.database.connection.databasename = graph.db
YOUR_DB_NAME.database.driver = org.structr.bolt.BoltDatabaseService
Migration Checklist for 4.x
- [ ] Install GraalVM as Java runtime
- [ ] Add
$.predicateprefix to all find/search predicates - [ ] Update Resource Permissions with visibility flags
- [ ] Split Resource Permissions that have both public and authenticated flags
- [ ] Prefix REST parameters with underscore
- [ ] Update date comparisons to use
getTime() - [ ] Review code for conditional chaining on ProxyObjects
- [ ] Update Neo4j configuration if upgrading database
- [ ] Update Cypher parameter syntax if using Neo4j 4.x