Skip to content

v0.55.0

Choose a tag to compare

@github-actions github-actions released this 03 Sep 22:57
· 2 commits to main since this release

0.55.0

Breaking Changes

  • Custom fields (experimental): a String value is capped at 256 characters, where it was previously unbounded, and is stored with surrounding whitespace trimmed. The cap is measured on the value as sent, so padding counts against it without being stored. A value that is blank once trimmed is rejected; pass None to clear the field instead.
  • Custom fields (experimental): a Timestamp value given as an all-digit string is read as epoch seconds, where it previously parsed as an ISO 8601 basic-format date. "20260101" now names an instant in August 1970 rather than 1 January 2026. Spell a date with separators — "2026-01-01" — to keep the previous reading.
  • A collection.collection_id (alias collection.id) filter on a Datasets, Events, or Files query accepts only EQUALS and NOT_EQUALS, and returns HTTP 400 for every other comparator. Previously none of these three query targets checked the field against Roboto's field catalog, so every comparator reached SQL: CONTAINS and NOT_CONTAINS matched against any substring of the ID; LIKE and NOT_LIKE compared through SQL LIKE, which is an exact comparison unless the value carries a % or _ wildcard; BEGINS_WITH compared through a wildcard LIKE pattern; GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, and LESS_THAN_OR_EQUAL ordered the ID as text; and IS_NULL, IS_NOT_NULL, EXISTS, and NOT_EXISTS tested it for null. All thirteen now return 400. This is the allow-list a Collections query and a Sessions query already applied to the same field; the three query targets that bypassed it now go through the same path. A collection ID is an opaque cl_-prefixed handle, so rewrite any of these filters to compare against the whole ID with EQUALS or NOT_EQUALS.
  • A filter value naming an instant before the Unix epoch returns HTTP 400 on the six timestamp fields that take one: Sessions min_timestamp_ns and max_timestamp_ns (aliased start_time and end_time), Events start_time and end_time, and Topics start_time and end_time. These columns count nanoseconds forward from 1970 and hold nothing negative, so a negative bound names no instant a caller could have meant. Previously a negative integer was passed through as written, and a negative float, numeric string, or Decimal, or a datetime earlier than 1970, was converted to a negative nanosecond count and compared, so a filter as ordinary-looking as start_time != -1 was accepted. The rejection reads the converted value rather than what you wrote, so every spelling of the same instant is refused the same way; -1 and -1.0 no longer take different paths.

Features Added

  • Metric.query (experimental): add time asc/desc sorting to the metrics query API backend endpoint.
  • Custom fields (experimental): a Timestamp value may be given as epoch seconds — a float, a decimal.Decimal, or a numeric string — alongside the datetime, ISO 8601 string, and epoch-nanoseconds int already accepted. A string is read as epoch seconds whenever it parses as a number, and as ISO 8601 only when it does not. A value that states no time zone — a naive datetime, or an ISO 8601 string carrying no offset — is read as UTC.
  • roboto.query.SavedFilters gains an eighth filter kind, identity, for the audit columns that hold a principal rather than a plain string (created_by, modified_by). Its operator names a principal type instead of a comparison: IS_USER, IS_DEVICE, IS_INVOCATION, IS_INTEGRATION, and IS_ORG each carry the principals of that type to match, and the valueless IS_ANY_USER, IS_ANY_DEVICE, IS_ANY_INVOCATION, IS_ANY_INTEGRATION, and IS_ANY_ORG match the whole type. These are roboto.query.IdentityComparator, not Comparator members, because a client expands them on the way to a query: IS_<TYPE> becomes EQUALS against each named principal (several are alternatives, so they fan out to an OR) and IS_ANY_<TYPE> becomes LIKE '<type>:%'. Values are roboto.query.LabeledOptions, each carrying the fully-qualified principal (user:<user_id>, device:<device_id>@<org_id>) alongside the display name shown when it was picked, since a principal ID is not a name a reader can place. A value whose type disagrees with the operator, or that is not a <type>:<id> principal at all, is rejected rather than stored as a filter that can never match.
  • Metric.query and Metric.aggregate (experimental) accept a condition, a single Condition or a nested ConditionGroup, so a metric can be read for only the sessions, devices, and collections you care about rather than every session in the time window. Every field must name the entity it filters on, in either the singular or the plural spelling: session.<field> (or sessions.<field>) and session.custom.<name> for the session a data point belongs to, device.<field> (or devices.<field>) and device.custom.<name> for the device that produced it, and collection.collection_id (or collections.collection_id, alias collection.id) and collection.custom.<name> for the collections the data point's session belongs to. The collection ID field accepts only EQUALS and NOT_EQUALS, the same two comparators it takes on every other query; any other comparator returns HTTP 400. A session belongs to any number of collections, so a collection condition quantifies over that set: a data point matches when its session belongs to at least one collection satisfying the condition, and a negated comparator means its session belongs to no collection satisfying the positive form, so a session in no collection at all matches every negated collection condition. On aggregate, the filter narrows what each bucket aggregates, and a bucket left with no matching data points is omitted.
  • Files can now be filtered by ingestion_status through RobotoSearch.find_files and the structured-query API, by equality or inequality against not_ingested, partly_ingested, or ingested. A file becomes partly_ingested as soon as a topic is recorded against it, and ingested only when a caller marks it so with File.mark_ingested() — conventionally the ingestion action that processed the file, though nothing enforces that. Only ingested makes a file eligible for post-ingestion triggers, so partly_ingested is where to look for files whose ingestion started but never reported completion. Files in formats Roboto does not ingest, such as images or PDFs, stay not_ingested unless a caller marks them ingested.
  • Sessions (experimental) can now be provided as inputs to action invocations, selected by session ID, session name, or RoboQL query. In the SDK that is InvocationInput.sessions, with the convenience factories InvocationInput.from_session_id and InvocationInput.session_query; action code reads the resolved Session entities off the InvocationContext as ctx.get_input().sessions. On the CLI, roboto actions invoke and roboto actions invoke-local take a repeatable --session-id and a --session-query, which combine as a union (each matched session runs once) and sit alongside --file-query and --topic-query in the selector-based input group, so one invocation can take files, topics, and sessions together; no flag in that group can be combined with --dataset or --file-path.

Bugs Fixed

  • Custom fields (experimental): a Timestamp value read back from an entity's custom_fields is an ISO 8601 string, and a UTC one is now spelled with a +00:00 offset instead of a Z suffix. datetime.datetime.fromisoformat rejects the Z form before Python 3.11, and we support Python 3.10. The instant is unchanged, and a value in another time zone keeps its own offset; only the spelling of UTC differs.
  • Custom fields (experimental): a Timestamp value outside the range a date and time can represent — year 1 through year 9999 — is rejected with a 400 naming the field and the offending value.
  • A timestamp filter value that a Sessions or Events query could not convert to epoch nanoseconds returned HTTP 500 for two families of bad input: a value that overflows during the conversion ("inf", "-inf", "infinity"), and a decimal exponent beyond what Python's decimal arithmetic will evaluate ("1E+999999"). Both now return HTTP 400 naming the field, as every other unconvertible value already did.
  • A boolean filter value on a Sessions, Events, or Topics timestamp field returns HTTP 400 rather than HTTP 500. RoboQL accepts a boolean literal on the right of any binary comparator, so start_time > true parses and used to reach Postgres, which has no comparison between a bigint column and a boolean and failed the request as an unhandled server error.
  • start_time and end_time on an Events query accept every shape roboto.time.Time accepts that names an instant at or after the Unix epoch: an integer (read as epoch nanoseconds), a float or Decimal (read as epoch seconds), an ISO8601 string, a "<sec>.<nsec>" string, or a datetime. Only strings were converted before; a float, Decimal, or datetime reached the comparison unchanged, to be compared against a column holding nanosecond counts.
  • start_time and end_time on a Topics query get that same conversion. They had none at all before, so any value other than an integer count of nanoseconds reached the comparison unchanged.
  • collection.id is accepted on a Datasets query, where it produces exactly the filter collection.collection_id does. An Events query and a Files query already accepted the alias; only a Datasets query turned it away, reporting that collection.id is not a valid field for the Datasets target.
  • A collection filter whose field name only begins with an accepted one is rejected rather than silently trimmed down to it. On a Datasets, Events, or Files query, collection.identifier was read as collection.id and collection.collection_id_v2 as collection.collection_id, dropping the rest of the name and filtering on a field the query never asked for.
  • A name-based topics or files input selector on an action invocation quotes each name the way RoboQL spells a string literal, so a name that used to produce a malformed query resolves. A files selector applied no escaping, so a double quote or a backslash in a name broke the query. A topics selector escaped as JSON, which spells those two characters as RoboQL does but renders every non-ASCII character (an accent, a CJK character, an emoji) as a \uXXXX escape, a form RoboQL has no syntax for. LIKE wildcards in a files name are unaffected: this changes how a name is spelled in the query, not how it is matched.
  • A double-quoted RoboQL string literal containing a control character returns HTTP 400 naming the literal, rather than HTTP 500. Carriage return and newline were already refused by the grammar; the other 30 codepoints below U+0020 passed the grammar and then failed JSON decoding as an unhandled server error. A double-quoted literal has no escape syntax for any of the 32, so none of them can be carried in one.
  • Dataset.upload_file resolves the File it returns through the file ID the upload itself reported, instead of looking the destination path up again. The file you get back is always the one this upload created; the path lookup could return a different file when a concurrent upload replaced the same destination in between. Reading an attribute off it now costs a single indexed lookup, where the path lookup's cost grew with the size of the dataset it ran against. Resolution stays lazy, so a caller that ignores the return value still pays no request at all. An upload that completes without reporting an ID for the file raises RobotoInternalException.
  • A BEGINS_WITH filter matches values that start with the operand. It previously matched values that ended with it: the SQL the query engine generated anchored its wildcard on the wrong side of the bound value, so metadata.serial BEGINS_WITH "RB-" compiled to the pattern %RB- and kept rows whose value finished with RB- while dropping every row the filter was asking for. The comparator is spelled startswith in the SDK's own client-side Condition.matches and as DynamoDB's begins_with on the query paths that go to that store, both of which were always right; only the Postgres path disagreed. A filter written against the old behavior, expecting a suffix match, should be rewritten as LIKE "%<value>".