diff --git a/.changeset/add-event-source-field.md b/.changeset/add-event-source-field.md new file mode 100644 index 00000000..7473c958 --- /dev/null +++ b/.changeset/add-event-source-field.md @@ -0,0 +1,5 @@ +--- +'java-sdk': minor +--- + +Add `source` field to `Event` to identify how the event was generated (`device` or `edge`) diff --git a/.schema-version b/.schema-version index 684a941c..c03b60b0 100644 --- a/.schema-version +++ b/.schema-version @@ -1 +1 @@ -v3.5.1 \ No newline at end of file +v3.6.0 \ No newline at end of file diff --git a/README.md b/README.md index d4ce86fb..1b9f791a 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,7 @@ Class | Method | HTTP request | Description - [EventRuleActionAllow](docs/EventRuleActionAllow.md) - [EventRuleActionBlock](docs/EventRuleActionBlock.md) - [EventSearch](docs/EventSearch.md) + - [EventSource](docs/EventSource.md) - [EventUpdate](docs/EventUpdate.md) - [FontPreferences](docs/FontPreferences.md) - [Geolocation](docs/Geolocation.md) diff --git a/docs/Event.md b/docs/Event.md index e5625501..e0416218 100644 --- a/docs/Event.md +++ b/docs/Event.md @@ -10,6 +10,7 @@ Contains results from Fingerprint Identification and all active Smart Signals. S |------------ | ------------- | ------------- | -------------| |**eventId** | **String** | Unique identifier of the user's request. The first portion of the event_id is a unix epoch milliseconds timestamp. | | |**timestamp** | **Long** | Timestamp of the event with millisecond precision in Unix time. | | +|**source** | [**EventSource**](EventSource.md) | | [optional] | |**incrementalIdentificationStatus** | [**IncrementalIdentificationStatus**](IncrementalIdentificationStatus.md) | | [optional] | |**linkedId** | **String** | A customer-provided id that was sent with the request. | [optional] | |**environmentId** | **String** | Environment Id of the event. | [optional] | diff --git a/docs/EventSource.md b/docs/EventSource.md new file mode 100644 index 00000000..55495668 --- /dev/null +++ b/docs/EventSource.md @@ -0,0 +1,19 @@ + + +# EventSource +Identifies how the event was generated. +- `device` - the event was generated by the JS agent or a mobile SDK running on an end-user device. +- `edge` - the event was generated by the Automation Intelligence API (`/edge` endpoint), analyzing a request intercepted at the edge. + + +## Enum + + +* `DEVICE` (value: `"device"`) + +* `EDGE` (value: `"edge"`) + +* `UNSUPPORTED_VALUE_SDK_UPGRADE_REQUIRED` (value: `"unsupported_value_sdk_upgrade_required"`) + + + diff --git a/examples/src/main/java/com/fingerprint/example/FunctionalTests.java b/examples/src/main/java/com/fingerprint/example/FunctionalTests.java index 650821a3..d0278f55 100644 --- a/examples/src/main/java/com/fingerprint/example/FunctionalTests.java +++ b/examples/src/main/java/com/fingerprint/example/FunctionalTests.java @@ -122,7 +122,16 @@ public static void main(String... args) { System.exit(1); } - api.getEvent(oldRequestId); + try { + api.getEvent(oldRequestId); + } catch (ApiException e) { + System.err.println( + "Exception when trying to request old event \"" + + oldRequestId + + "\":" + + e.getMessage()); + System.exit(1); + } System.out.println("Old events are good"); } catch (ApiException e) { System.err.println("Exception when trying to read old data:" + e.getMessage()); diff --git a/res/fingerprint-server-api.yaml b/res/fingerprint-server-api.yaml index 93b259db..c6cb08f2 100644 --- a/res/fingerprint-server-api.yaml +++ b/res/fingerprint-server-api.yaml @@ -1178,6 +1178,19 @@ components: format: int64 examples: - 1708102555327 + EventSource: + type: string + description: > + Identifies how the event was generated. + + - `device` - the event was generated by the JS agent or a mobile SDK + running on an end-user device. + + - `edge` - the event was generated by the Automation Intelligence API + (`/edge` endpoint), analyzing a request intercepted at the edge. + enum: + - device + - edge Url: type: string examples: @@ -3084,6 +3097,12 @@ components: - android - ios - browser + source: + $ref: '#/components/schemas/EventSource' + x-platforms: + - android + - ios + - browser incremental_identification_status: $ref: '#/components/schemas/IncrementalIdentificationStatus' x-platforms: diff --git a/sdk/src/main/java/com/fingerprint/v4/model/Event.java b/sdk/src/main/java/com/fingerprint/v4/model/Event.java index 2fee25fd..923ef8b6 100644 --- a/sdk/src/main/java/com/fingerprint/v4/model/Event.java +++ b/sdk/src/main/java/com/fingerprint/v4/model/Event.java @@ -27,6 +27,7 @@ @JsonPropertyOrder({ Event.JSON_PROPERTY_EVENT_ID, Event.JSON_PROPERTY_TIMESTAMP, + Event.JSON_PROPERTY_SOURCE, Event.JSON_PROPERTY_INCREMENTAL_IDENTIFICATION_STATUS, Event.JSON_PROPERTY_LINKED_ID, Event.JSON_PROPERTY_ENVIRONMENT_ID, @@ -100,6 +101,9 @@ public class Event { public static final String JSON_PROPERTY_TIMESTAMP = "timestamp"; @jakarta.annotation.Nonnull private Long timestamp; + public static final String JSON_PROPERTY_SOURCE = "source"; + @jakarta.annotation.Nullable private EventSource source; + public static final String JSON_PROPERTY_INCREMENTAL_IDENTIFICATION_STATUS = "incremental_identification_status"; @@ -337,6 +341,28 @@ public void setTimestamp(@jakarta.annotation.Nonnull Long timestamp) { this.timestamp = timestamp; } + public Event source(@jakarta.annotation.Nullable EventSource source) { + this.source = source; + return this; + } + + /** + * Get source + * @return source + */ + @jakarta.annotation.Nullable + @JsonProperty(value = JSON_PROPERTY_SOURCE, required = false) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public EventSource getSource() { + return source; + } + + @JsonProperty(value = JSON_PROPERTY_SOURCE, required = false) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public void setSource(@jakarta.annotation.Nullable EventSource source) { + this.source = source; + } + public Event incrementalIdentificationStatus( @jakarta.annotation.Nullable IncrementalIdentificationStatus incrementalIdentificationStatus) { @@ -1751,6 +1777,7 @@ public boolean equals(Object o) { Event event = (Event) o; return Objects.equals(this.eventId, event.eventId) && Objects.equals(this.timestamp, event.timestamp) + && Objects.equals(this.source, event.source) && Objects.equals( this.incrementalIdentificationStatus, event.incrementalIdentificationStatus) && Objects.equals(this.linkedId, event.linkedId) @@ -1821,6 +1848,7 @@ public int hashCode() { return Objects.hash( eventId, timestamp, + source, incrementalIdentificationStatus, linkedId, environmentId, @@ -1891,6 +1919,7 @@ public String toString() { sb.append("class Event {\n"); sb.append(" eventId: ").append(toIndentedString(eventId)).append("\n"); sb.append(" timestamp: ").append(toIndentedString(timestamp)).append("\n"); + sb.append(" source: ").append(toIndentedString(source)).append("\n"); sb.append(" incrementalIdentificationStatus: ") .append(toIndentedString(incrementalIdentificationStatus)) .append("\n"); diff --git a/sdk/src/main/java/com/fingerprint/v4/model/EventSource.java b/sdk/src/main/java/com/fingerprint/v4/model/EventSource.java new file mode 100644 index 00000000..02a4d0d2 --- /dev/null +++ b/sdk/src/main/java/com/fingerprint/v4/model/EventSource.java @@ -0,0 +1,53 @@ +/* + * Server API + * Fingerprint Server API allows you to get, search, and update Events in a server environment. It can be used for data exports, decision-making, and data analysis scenarios. Server API is intended for server-side usage, it's not intended to be used from the client side, whether it's a browser or a mobile device. The API also supports collection of Automation Intelligence for requests to your server in edge, pre-origin, or middleware contexts. + * + * The version of the OpenAPI document: 4 + * Contact: support@fingerprint.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package com.fingerprint.v4.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Identifies how the event was generated. - `device` - the event was generated by the JS agent or a mobile SDK running on an end-user device. - `edge` - the event was generated by the Automation Intelligence API (`/edge` endpoint), analyzing a request intercepted at the edge. + */ +public enum EventSource { + DEVICE("device"), + + EDGE("edge"), + + UNSUPPORTED_VALUE_SDK_UPGRADE_REQUIRED("unsupported_value_sdk_upgrade_required"); + + private String value; + + EventSource(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static EventSource fromValue(String value) { + for (EventSource b : EventSource.values()) { + if (b.value.equals(value)) { + return b; + } + } + return UNSUPPORTED_VALUE_SDK_UPGRADE_REQUIRED; + } +} diff --git a/sdk/src/test/java/com/fingerprint/v4/SerializationTest.java b/sdk/src/test/java/com/fingerprint/v4/SerializationTest.java index 447363ad..8bfe0689 100644 --- a/sdk/src/test/java/com/fingerprint/v4/SerializationTest.java +++ b/sdk/src/test/java/com/fingerprint/v4/SerializationTest.java @@ -1,23 +1,41 @@ package com.fingerprint.v4; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fingerprint.v4.model.ErrorCode; +import com.fingerprint.v4.model.ErrorResponse; import com.fingerprint.v4.model.Event; import com.fingerprint.v4.model.EventRuleAction; import com.fingerprint.v4.sdk.JSON; +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.TestInstance; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class SerializationTest { + private static final String ERRORS_DIR = "mocks/errors"; + private static final String EVENTS_DIR = "mocks/events"; + private InputStream getFileAsIOStream(final String fileName) { InputStream ioStream = this.getClass().getClassLoader().getResourceAsStream(fileName); @@ -35,17 +53,6 @@ private static ObjectMapper getMapper() { return mapper; } - @Test - public void deserializeSerializeEvent() throws IOException { - ObjectMapper sdkObjectMapper = JSON.getDefault().getMapper(); - Event event = - sdkObjectMapper.readValue( - getFileAsIOStream("mocks/events/get_event_200.json"), Event.class); - - ObjectMapper springLikeObjectMapper = getMapper(); - springLikeObjectMapper.writeValueAsString(event); - } - @Test public void deserializeEventWithUnknownBotResultValue() throws IOException { ObjectMapper sdkObjectMapper = JSON.getDefault().getMapper(); @@ -96,4 +103,86 @@ public void deserializeEventWithUnknownRuleActionTypeValue() throws IOException assertInstanceOf(EventRuleAction.UnknownEventRuleAction.class, event.getRuleAction()); } + + @TestFactory + public Stream deserializeErrorResponses() { + ObjectMapper sdkObjectMapper = JSON.getDefault().getMapper(); + + return listMockFileNames(ERRORS_DIR, name -> true).stream() + .map( + fileName -> + DynamicTest.dynamicTest( + fileName, + () -> { + String resource = ERRORS_DIR + "/" + fileName; + + ObjectNode errorNode = + sdkObjectMapper.readValue(getFileAsIOStream(resource), ObjectNode.class); + ErrorResponse errorResponse = + sdkObjectMapper.readValue( + getFileAsIOStream(resource), ErrorResponse.class); + + assertNotNull(errorResponse.getError()); + assertNotEquals( + ErrorCode.UNSUPPORTED_VALUE_SDK_UPGRADE_REQUIRED, + errorResponse.getError().getCode(), + "Unknown error code in " + resource); + assertEquals( + errorNode.at("/error/code").asText(), + errorResponse.getError().getCode().getValue()); + assertEquals( + errorNode.at("/error/message").asText(), + errorResponse.getError().getMessage()); + + getMapper().writeValueAsString(errorResponse); + })); + } + + @TestFactory + public Stream deserializeSerializeEvents() { + ObjectMapper sdkObjectMapper = JSON.getDefault().getMapper(); + + return listMockFileNames(EVENTS_DIR, name -> name.startsWith("get_event")).stream() + .map( + fileName -> + DynamicTest.dynamicTest( + fileName, + () -> { + String resource = EVENTS_DIR + "/" + fileName; + + ObjectNode eventNode = + sdkObjectMapper.readValue(getFileAsIOStream(resource), ObjectNode.class); + Event event = + sdkObjectMapper.readValue(getFileAsIOStream(resource), Event.class); + + assertEquals(eventNode.get("event_id").asText(), event.getEventId()); + assertEquals( + eventNode.get("timestamp").asLong(), event.getTimestamp().longValue()); + + getMapper().writeValueAsString(event); + })); + } + + private List listMockFileNames(String directoryName, Predicate fileNameFilter) { + URL directory = this.getClass().getClassLoader().getResource(directoryName); + + if (directory == null) { + throw new IllegalStateException(directoryName + " is not found"); + } + + File[] files; + try { + files = + new File(directory.toURI()) + .listFiles((dir, name) -> name.endsWith(".json") && fileNameFilter.test(name)); + } catch (URISyntaxException e) { + throw new IllegalStateException(e); + } + + if (files == null || files.length == 0) { + throw new IllegalStateException("No mocks found in " + directoryName); + } + + return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList()); + } } diff --git a/sdk/src/test/resources/mocks/errors/429_too_many_search_requests.json b/sdk/src/test/resources/mocks/errors/429_too_many_search_requests.json new file mode 100644 index 00000000..bc42b32a --- /dev/null +++ b/sdk/src/test/resources/mocks/errors/429_too_many_search_requests.json @@ -0,0 +1,6 @@ +{ + "error": { + "code": "too_many_requests", + "message": "too many search requests" + } +} diff --git a/sdk/src/test/resources/mocks/errors/504_search_timeout_exceeded.json b/sdk/src/test/resources/mocks/errors/504_search_timeout_exceeded.json new file mode 100644 index 00000000..cc5fcbd7 --- /dev/null +++ b/sdk/src/test/resources/mocks/errors/504_search_timeout_exceeded.json @@ -0,0 +1,6 @@ +{ + "error": { + "code": "failed", + "message": "gateway timeout" + } +} diff --git a/sdk/src/test/resources/mocks/events/get_event_200.json b/sdk/src/test/resources/mocks/events/get_event_200.json index 8c40bcd6..6d6c726e 100644 --- a/sdk/src/test/resources/mocks/events/get_event_200.json +++ b/sdk/src/test/resources/mocks/events/get_event_200.json @@ -3,6 +3,7 @@ "tags": {}, "timestamp": 1708102555327, "event_id": "1708102555327.NLOjmg", + "source": "device", "incremental_identification_status": "completed", "url": "https://www.example.com/login?hope{this{works[!", "ip_address": "61.127.217.15", diff --git a/sdk/src/test/resources/mocks/events/get_event_200_with_unknown_field.json b/sdk/src/test/resources/mocks/events/get_event_200_with_unknown_field.json index b87f4864..bdbf8715 100644 --- a/sdk/src/test/resources/mocks/events/get_event_200_with_unknown_field.json +++ b/sdk/src/test/resources/mocks/events/get_event_200_with_unknown_field.json @@ -3,6 +3,7 @@ "tags": {}, "timestamp": 1708102555327, "event_id": "1708102555327.NLOjmg", + "source": "device", "incremental_identification_status": "completed", "url": "https://www.example.com/login?hope{this{works[!", "ip_address": "61.127.217.15", diff --git a/sdk/src/test/resources/mocks/events/get_event_ruleset_200.json b/sdk/src/test/resources/mocks/events/get_event_ruleset_200.json index b312bfe5..1e5337c0 100644 --- a/sdk/src/test/resources/mocks/events/get_event_ruleset_200.json +++ b/sdk/src/test/resources/mocks/events/get_event_ruleset_200.json @@ -3,6 +3,7 @@ "tags": {}, "timestamp": 1708102555327, "event_id": "1708102555327.NLOjmg", + "source": "device", "incremental_identification_status": "completed", "url": "https://www.example.com/login?hope{this{works[!", "ip_address": "61.127.217.15", diff --git a/sdk/src/test/resources/mocks/events/get_event_with_bot_info_200.json b/sdk/src/test/resources/mocks/events/get_event_with_bot_info_200.json index 5b5427aa..b44a687a 100644 --- a/sdk/src/test/resources/mocks/events/get_event_with_bot_info_200.json +++ b/sdk/src/test/resources/mocks/events/get_event_with_bot_info_200.json @@ -3,6 +3,7 @@ "tags": {}, "timestamp": 1708102555327, "event_id": "1708102555327.NLOjmg", + "source": "device", "incremental_identification_status": "completed", "url": "https://www.example.com/login?hope{this{works[!", "ip_address": "61.127.217.15", diff --git a/sdk/src/test/resources/mocks/events/get_event_with_edge_200.json b/sdk/src/test/resources/mocks/events/get_event_with_edge_200.json new file mode 100644 index 00000000..387c86eb --- /dev/null +++ b/sdk/src/test/resources/mocks/events/get_event_with_edge_200.json @@ -0,0 +1,51 @@ +{ + "event_id": "1758130560902.8tRtrH", + "timestamp": 1758130560902, + "source": "edge", + "url": "https://example.com/login?foo=bar", + "bot_info": { + "category": "ai_agent", + "provider": "OpenAI", + "provider_url": "https://openai.com", + "name": "ChatGPT Agent", + "identity": "signed", + "confidence": "high" + }, + "ip_info": { + "v4": { + "address": "104.210.139.192", + "geolocation": { + "accuracy_radius": 20, + "latitude": 29.42412, + "longitude": -98.49363, + "postal_code": "78205", + "timezone": "America/Chicago", + "city_name": "San Antonio", + "country_code": "US", + "country_name": "United States", + "continent_code": "NA", + "continent_name": "North America", + "subdivisions": [ + { + "iso_code": "TX", + "name": "Texas" + } + ] + }, + "asn": "8075", + "asn_name": "Microsoft Corporation", + "asn_network": "104.208.0.0/13", + "asn_type": "hosting", + "datacenter_result": true, + "datacenter_name": "Microsoft Azure" + } + }, + "proxy": false, + "proxy_confidence": "high", + "vpn": false, + "vpn_confidence": "medium", + "vpn_methods": { + "public_vpn": false, + "relay": false + } +} \ No newline at end of file diff --git a/sdk/src/test/resources/mocks/events/search/get_event_search_200.json b/sdk/src/test/resources/mocks/events/search/get_event_search_200.json index ca87827b..daad06b0 100644 --- a/sdk/src/test/resources/mocks/events/search/get_event_search_200.json +++ b/sdk/src/test/resources/mocks/events/search/get_event_search_200.json @@ -5,6 +5,7 @@ "tags": {}, "timestamp": 1708102555327, "event_id": "1708102555327.NLOjmg", + "source": "device", "incremental_identification_status": "completed", "url": "https://www.example.com/login?hope{this{works[!", "ip_address": "61.127.217.15",