diff --git a/README.adoc b/README.adoc index b46ad24..9c1607c 100644 --- a/README.adoc +++ b/README.adoc @@ -8,15 +8,13 @@ Per Minborg :toc-title: Table of contents image:https://maven-badges.herokuapp.com/maven-central/net.openhft/chronicle-analytics/badge.svg[Maven Central,link=https://maven-badges.herokuapp.com/maven-central/net.openhft/chronicle-analytics] -image:https://javadoc-badge.appspot.com/net.openhft/chronicle-analytics.svg?label=javadoc[JavaDoc, link=https://www.javadoc.io/doc/net.openhft/chronicle-analytics] -image:https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000[License, link=https://github.com/OpenHFT/Chronicle-Analytics/blob/master/LICENSE] +image:https://javadoc-badge.appspot.com/net.openhft/chronicle-analytics.svg?label=javadoc[JavaDoc,link=https://www.javadoc.io/doc/net.openhft/chronicle-analytics] +image:https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000[License,link=https://github.com/OpenHFT/Chronicle-Analytics/blob/master/LICENSE] image:https://img.shields.io/gitter/room/OpenHFT/Lobby.svg?style=popout[link="https://gitter.im/OpenHFT/Lobby"] -This library provides remote ingress to Google Analytics 4, allowing data, such as usage-statistics, to be collected for Java applications.The data can subsequently be analysed using -a variety of tools available from Google and other providers. +This library provides remote ingress to Google Analytics 4, allowing data, such as usage-statistics, to be collected for Java applications.The data can subsequently be analysed using a variety of tools available from Google and other providers. -Here is a short Java snippet showing how one could use the library to send a Google Analytics event -for the measurement id "G-TDAZG4CU3G" using the api secret "k2hL3x2dQaKq9F2gQ-PNhQ". +Here is a short Java snippet showing how one could use the library to send a Google Analytics event for the measurement id "G-TDAZG4CU3G" using the api secret "k2hL3x2dQaKq9F2gQ-PNhQ". [source,java] ---- @@ -35,7 +33,7 @@ toc::[] Under `noop` there is a build for an empty jar which when included will ensure this jar does nothing. This jar is redundant but can help ensure this jar does nothing. -To disable this jar include the following in Maven: +To disable this jar include the following in Maven: [script,xml] ---- @@ -45,7 +43,7 @@ To disable this jar include the following in Maven: 0.20.0 ---- -To disable this library with Gradle, use: +To disable this library with Gradle, use: [script,xml] ---- @@ -125,7 +123,7 @@ NOTE: See the link:https://javadoc.io/doc/net.openhft/chronicle-analytics/latest The following example sets up an analytics instance with the *event parameter* `app_version = 1.4.2` and perhaps the *user properties* `os_name = Linux`, `os_version = 4.18.0.147.2.1.2l8_1.x86_64` and `java_runtime_version = 1.8.0_272-b10` depending on the environment used: -[source, java] +[source,java] ---- public class AnalyticsExampleMain { @@ -172,7 +170,9 @@ The library does not have any transitive dependencies and depends directly only === JSON compliance -The library supports basic JSON functionality. Escaping works for the most common characters used in the English language. To keep the dependency graph simple, we did not depend on any external JSON library. +The library supports basic JSON functionality. +Escaping works for the most common characters used in the English language. +To keep the dependency graph simple, we did not depend on any external JSON library. === Thread safety @@ -180,8 +180,10 @@ Analytics instances are thread-safe and can be shared across threads. === Thread usage -The library is using a single thread named `"chronicle-analytics-http-client"` to send requests. This thread is initially started on demand and will remain dormant throughout the lifespan of the JVM. +The library is using a single thread named `"chronicle-analytics-http-client"` to send requests. +This thread is initially started on demand and will remain dormant throughout the lifespan of the JVM. === Special empty version -There is a special empty artifact available with the version number `0.EMPTY`. This version can be used to remove analytics code from projects that depends on analytics. +There is a special empty artifact available with the version number `0.EMPTY`. +This version can be used to remove analytics code from projects that depends on analytics. diff --git a/noop/pom.xml b/noop/pom.xml index b225d51..9d22459 100644 --- a/noop/pom.xml +++ b/noop/pom.xml @@ -1,6 +1,6 @@ - + 4.0.0 @@ -23,7 +24,7 @@ net.openhft java-parent-pom 1.27ea1 - + chronicle-analytics 2.27ea2-SNAPSHOT @@ -31,6 +32,18 @@ Chronicle-Analytics bundle + + 3.6.0 + 8.45.1 + 4.8.6.6 + 1.14.0 + 3.28.0 + 0.8.14 + 0.9102 + 0.7142857 + 1.23ea6 + + @@ -75,6 +88,12 @@ test + + org.junit.vintage + junit-vintage-engine + test + + com.squareup.okhttp3 mockwebserver @@ -181,26 +200,6 @@ - - - org.jacoco - jacoco-maven-plugin - - - - prepare-agent - - - - report - prepare-package - - report - - - - - org.sonarsource.scanner.maven sonar-maven-plugin @@ -265,6 +264,147 @@ + + code-review + + false + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle.version} + + + com.puppycrawl.tools + checkstyle + ${puppycrawl.version} + + + net.openhft + chronicle-quality-rules + ${chronicle-quality-rules.version} + + + + + checkstyle + verify + + check + + + + + src/main/config/checkstyle.xml + true + true + warning + + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs.version} + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs.version} + + + + + spotbugs + verify + + check + + + + + Max + Low + true + src/main/config/spotbugs-exclude.xml + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs.version} + + + + + + org.apache.maven.plugins + maven-pmd-plugin + ${maven-pmd-plugin.version} + + + pmd + verify + + check + + + + + true + true + src/main/config/pmd-exclude.properties + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + check + verify + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + ${jacoco.line.coverage} + + + BRANCH + COVEREDRATIO + ${jacoco.branch.coverage} + + + + + + + + + + + diff --git a/src/main/Java11/module-info.java.txt b/src/main/Java11/module-info.java.txt index 7308818..2cd568a 100644 --- a/src/main/Java11/module-info.java.txt +++ b/src/main/Java11/module-info.java.txt @@ -1,7 +1,5 @@ /* - * Copyright 2016-2020 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/adoc/project-requirements.adoc b/src/main/adoc/project-requirements.adoc new file mode 100644 index 0000000..d989d65 --- /dev/null +++ b/src/main/adoc/project-requirements.adoc @@ -0,0 +1,82 @@ += Chronicle Analytics – Project Requirements +:lang: en-GB +:toc: +:sectnums: + +// ------------------------------------------------------------------- +// Identifier legend +// ------------------------------------------------------------------- +// Scope = CA (Chronicle Analytics) +// Tags = FN (Functional), NF-P (Performance), NF-O (Operational), +// DOC (Documentation), TEST (Testing), SEC (Security) +// IDs = three digit sequence numbers per tag. +// ------------------------------------------------------------------- + +== Scope + +Chronicle Analytics provides a lightweight client for emitting usage events to Google Analytics Universal Analytics (GA3) and Google Analytics 4 (GA4). +It is designed for embedding inside other Chronicle libraries that want optional outbound telemetry while keeping dependencies, footprint, and runtime overhead to a minimum. + +The requirements below define the minimum functionality expected from the module and the controls callers rely on when enabling analytics. + +== Functional Requirements + +[cols="1,5,3",options="header"] +|=== +|ID |Description |Verification +|CA-FN-001 |Expose `Analytics` interface offering `sendEvent(String)` and +`sendEvent(String, Map)`. |Unit tests (`AnalyticsTest`) cover default delegation. +|CA-FN-002 |Provide `Analytics.builder(String measurementId, String apiSecret)` returning a +single-use builder that produces an `Analytics` instance. |Unit test exercises build flow. +|CA-FN-003 |Ensure the builder supports user properties, event parameters, URL override, custom +loggers, frequency limiting, and client-id file name override. |`VanillaAnalyticsBuilderTest` exercises each mutator. +|CA-FN-004 |Auto-select GA3 transport when the measurement id starts with `UA-` and GA4 transport +otherwise. |Unit test forces both branches. +|CA-FN-005 |Detect active JUnit classes on the classpath and return the mute implementation unless +`withReportDespiteJUnit()` is set. |Test covers default mute behaviour. +|CA-FN-006 |Persist a stable client identifier across runs using the configured client-id file and +create one when missing. |`ClientIdUtilTest` exercises generation and reuse. +|CA-FN-007 |Respect `withFrequencyLimit(int,long,TimeUnit)` by dropping events beyond the configured +rate within the duration window. |`GoogleAnalyticsTest` covers rate limiting reset and cap. +|CA-FN-008 |HTTP publishing must be asynchronous and never block the caller thread beyond the +attempt-to-send eligibility checks. |Implementation delegates to single-thread executor. +|CA-FN-009 |Provide a mute implementation that records drops for testing while emitting no +network traffic. |`MuteAnalyticsTest` verifies counter changes. +|CA-FN-010 |Surface builder overrides for custom GA endpoint URLs to support on-premise or proxy +deployments. |Unit test asserts setter round-trip. +|=== + +== Non-Functional Requirements + +[cols="1,5,3",options="header"] +|=== +|ID |Requirement |Rationale +|CA-NF-P-001 |Invoking `sendEvent` must add no more than a few microseconds of overhead when the +event is rejected by throttling or muted (no network call). |Library must remain safe to call on +hot paths. +|CA-NF-P-002 |All network I/O is delegated to a daemon thread to avoid blocking shutdown. |Prevents +Analytics from extending application lifetime. +|CA-NF-O-001 |The client-id persistence file path defaults to `${user.home}/.chronicle.analytics.client.id` +and must be configurable. |Allows relocation for locked-down environments. +|CA-NF-O-002 |Analytics honours the system property `chronicle.analytics.disable=true` (via caller +contract) and must continue to operate safely if callers instantiate the builder regardless. |Ensures +callers can globally opt out. +|CA-NF-SEC-001 |HTTP requests must be sent over `https` by default. |Protect event payloads in transit. +|CA-DOC-001 |Document builder options, mute behaviour, and opt-out controls for downstream +consumers. |Empowers adopters to configure safely. +|CA-TEST-001 |Unit tests exercise the main builder pathways, frequency limiting, and mute behaviour. +|Maintains confidence in telemetry guardrails. +|=== + +== External Interfaces & Dependencies + +* Java Platform (minimum baseline matches parent BOM requirements). +* Optional presence of JUnit 4 or JUnit 5 triggers muted mode by default. +* Outbound HTTPS access to Google Analytics endpoints or a user-specified URL. +* Local filesystem write access to the configured client-id persistence file. + +== Assumptions & Out-of-Scope + +* Chronicle Analytics does not guarantee delivery; upstream outages or network failures are logged only via the configured error logger. +* Aggregation, reporting dashboards, or offline storage are handled by Google Analytics or custom infrastructure, not by this module. +* No additional instrumentation or sampling logic is provided beyond simple frequency limiting. diff --git a/src/main/adoc/security-review.adoc b/src/main/adoc/security-review.adoc new file mode 100644 index 0000000..9b6acf3 --- /dev/null +++ b/src/main/adoc/security-review.adoc @@ -0,0 +1,65 @@ += Chronicle Analytics – Security Review +:lang: en-GB +:toc: +:sectnums: + +== Introduction + +Chronicle Analytics emits optional telemetry to Google Analytics. +The library favours minimal dependencies and low-latency behaviour, which introduces several deliberate trade-offs. +This review captures expected data flows, known risks, and the mitigations that callers or library maintainers must provide. + +== Data Flow Summary + +* `Analytics.sendEvent` assembles a small payload containing the event name, optional event parameters, and any configured user properties. +* A client identifier is loaded from `${clientIdFileName}` (defaults to a hidden file in +`${user.home}`) and created if missing. +* Google Analytics 3 requests use the Measurement Protocol via HTTPS POST to +`https://www.google-analytics.com/collect`. +* Google Analytics 4 requests build a JSON payload and POST to +`https://www.google-analytics.com/mp/collect` (overrideable through the builder). +* All HTTP dispatch happens on a single daemon thread; the caller does not await responses. + +== Known Risks & Mitigations + +[cols="2,5",options="header"] +|=== +|Risk |Mitigation +|Client identifier file is created in the user home directory and may reveal telemetry usage. +|Document the location and allow overriding via `withClientIdFileName`. Clean up as part of +uninstallation if required. +|Outbound HTTPS endpoint is configurable and could be pointed at an unexpected host. +|Callers should only override the URL for trusted proxies. Use dependency injection to limit who can +supply builder configuration. +|Event names and parameters are caller-provided strings and may include sensitive data. +|Upstream libraries must scrub personally identifiable or confidential information before calling +the API. Provide configuration toggles to disable analytics at runtime. +|Asynchronous sender uses a shared single-thread executor – exceptions are only surfaced via the +error logger. +|Supply a non no-op error logger in production so that I/O failures can be traced. Consider routing +to SLF4J or structured logging. +|Lack of transport retries could drop events under transient network failure. +|Accept the loss as part of the "best effort" contract. For critical metrics, integrate with a +higher-reliability telemetry pipeline instead. +|Builder currently relies on caller honouring `chronicle.analytics.disable=true`. +|Downstream libraries must guard their own builder usage with a property check. Future work may +centralise the check inside `VanillaAnalyticsBuilder`. +|JUnit detection suppresses analytics, but tests that manually enable reporting could leak events. +|Ensure CI environments leave the default mute behaviour in place; only set `withReportDespiteJUnit` +for intentional integration tests targeting GA sandboxes. +|=== + +== Operational Guidance + +* Configure the error and debug loggers to integrate with the host application logging framework. +* When embedding inside other Chronicle projects, surface a visible opt-out knob that sets `chronicle.analytics.disable=true` before the builder is invoked. +* Regularly review filesystem permissions on the client-id file to ensure it is not world-readable on shared hosts. +* For geographically restricted deployments, validate that outbound HTTPS to the configured endpoint is permitted; otherwise disable analytics. + +== Review Cadence + +This document should be revisited whenever: + +* A new transport or analytics provider is added. +* Additional metadata is captured or persisted. +* Error handling, logging, or client-id generation behaviour changes materially. diff --git a/src/main/config/checkstyle.xml b/src/main/config/checkstyle.xml new file mode 100644 index 0000000..844dd90 --- /dev/null +++ b/src/main/config/checkstyle.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/config/pmd-exclude.properties b/src/main/config/pmd-exclude.properties new file mode 100644 index 0000000..af72877 --- /dev/null +++ b/src/main/config/pmd-exclude.properties @@ -0,0 +1,4 @@ +# PMD exclusions with justifications +# Format: filepath=rule1,rule2 +# Example: +# net/openhft/chronicle/analytics/internal/LegacyAdapter.java=AvoidReassigningParameters,TooManyFields diff --git a/src/main/config/spotbugs-exclude.xml b/src/main/config/spotbugs-exclude.xml new file mode 100644 index 0000000..8cc61a8 --- /dev/null +++ b/src/main/config/spotbugs-exclude.xml @@ -0,0 +1,32 @@ + + + + + + + + + ANALYTICS-SEC-201: HTTP client restricted to Chronicle analytics endpoints; SSRF remediation tracked + separately. + + + + + + + Client id path validated via sanitize(); remaining uses are constrained to the caller-controlled + persistence location. + + + + + + + Builder exposes client id file override; sanitize() enforces normalized, absolute paths before use. + + + + diff --git a/src/main/java/net/openhft/chronicle/analytics/Analytics.java b/src/main/java/net/openhft/chronicle/analytics/Analytics.java index 7f422aa..67ea089 100644 --- a/src/main/java/net/openhft/chronicle/analytics/Analytics.java +++ b/src/main/java/net/openhft/chronicle/analytics/Analytics.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2020 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,7 +62,7 @@ default void sendEvent(@NotNull String name) { * The builder can only create one single Analytic instance. * * @param measurementId to use for reporting - * @param apiSecret to use for reporting + * @param apiSecret to use for reporting * @return a new Builder that can be used to create an Analytic instance */ @NotNull diff --git a/src/main/java/net/openhft/chronicle/analytics/internal/AbstractGoogleAnalytics.java b/src/main/java/net/openhft/chronicle/analytics/internal/AbstractGoogleAnalytics.java index 73746c8..f6deef7 100644 --- a/src/main/java/net/openhft/chronicle/analytics/internal/AbstractGoogleAnalytics.java +++ b/src/main/java/net/openhft/chronicle/analytics/internal/AbstractGoogleAnalytics.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2020 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/net/openhft/chronicle/analytics/internal/AnalyticsConfiguration.java b/src/main/java/net/openhft/chronicle/analytics/internal/AnalyticsConfiguration.java index 4efa1c2..d6ea87e 100644 --- a/src/main/java/net/openhft/chronicle/analytics/internal/AnalyticsConfiguration.java +++ b/src/main/java/net/openhft/chronicle/analytics/internal/AnalyticsConfiguration.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2020 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,14 +23,24 @@ public interface AnalyticsConfiguration { @NotNull String measurementId(); + @NotNull String apiSecret(); + @NotNull Map userProperties(); + @NotNull Map eventParameters(); + @NotNull Consumer errorLogger(); + @NotNull Consumer debugLogger(); + long duration(); + int messages(); + @NotNull TimeUnit timeUnit(); + @NotNull String clientIdFileName(); + @NotNull String url(); } diff --git a/src/main/java/net/openhft/chronicle/analytics/internal/FilesUtil.java b/src/main/java/net/openhft/chronicle/analytics/internal/FilesUtil.java index 4dc5248..78b8d65 100644 --- a/src/main/java/net/openhft/chronicle/analytics/internal/FilesUtil.java +++ b/src/main/java/net/openhft/chronicle/analytics/internal/FilesUtil.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +19,7 @@ import org.jetbrains.annotations.NotNull; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -42,7 +41,7 @@ enum FilesUtil { // user's home directory. If that fails, a new random clientId // is generated and an attempt is made to save it in said file. static String acquireClientId(@NotNull final String clientIdFileName, @NotNull final Consumer debugLogger) { - final Path path = Paths.get(clientIdFileName); + final Path path = sanitize(clientIdFileName); try { try (Stream lines = Files.lines(path, UTF_8)) { return lines @@ -51,7 +50,7 @@ static String acquireClientId(@NotNull final String clientIdFileName, @NotNull f .orElseThrow(NoSuchElementException::new) .toString(); } - } catch (Exception e) { + } catch (IOException | UncheckedIOException | IllegalArgumentException | NoSuchElementException e) { debugLogger.accept("Client id file not present: " + path.toAbsolutePath() + ' ' + e); } final String id = UUID.randomUUID().toString(); @@ -70,7 +69,7 @@ static boolean isSameAsLastUsedFileTimeStampSecond(final int currentSecondOfDay) .findFirst() .map(Integer::parseInt) .orElse(0); - final boolean same = (secondOfDay == currentSecondOfDay); + final boolean same = secondOfDay == currentSecondOfDay; if (!same) { // We are on a new second so store the new second touchLastContent(path); @@ -101,10 +100,13 @@ static void removeLastUsedFileTimeStampSecond() { } private static Path lastPath() { - final String fileName = Optional.ofNullable(System.getProperty("user.home")) - .orElse(".") + - CHRONICLE_ANALYTICS_LAST_FILE_NAME; - return Paths.get(fileName); + final Path home = Paths.get(Optional.ofNullable(System.getProperty("user.home")).orElse(".")).toAbsolutePath().normalize(); + return home.resolve(CHRONICLE_ANALYTICS_LAST_FILE_NAME.substring(1)); + } + + private static Path sanitize(String candidate) { + Path normalized = Paths.get(candidate).normalize(); + return normalized.isAbsolute() ? normalized : normalized.toAbsolutePath().normalize(); } } diff --git a/src/main/java/net/openhft/chronicle/analytics/internal/GoogleAnalytics3.java b/src/main/java/net/openhft/chronicle/analytics/internal/GoogleAnalytics3.java index a1b1f34..7a9dbe3 100644 --- a/src/main/java/net/openhft/chronicle/analytics/internal/GoogleAnalytics3.java +++ b/src/main/java/net/openhft/chronicle/analytics/internal/GoogleAnalytics3.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2020 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +15,6 @@ */ package net.openhft.chronicle.analytics.internal; -import net.openhft.chronicle.analytics.Analytics; import org.jetbrains.annotations.NotNull; import java.util.LinkedHashMap; @@ -26,7 +23,7 @@ import java.util.StringJoiner; import java.util.concurrent.atomic.AtomicInteger; -final class GoogleAnalytics3 extends AbstractGoogleAnalytics implements Analytics { +final class GoogleAnalytics3 extends AbstractGoogleAnalytics { private static final String URL_STRING = "https://www.google-analytics.com/collect"; diff --git a/src/main/java/net/openhft/chronicle/analytics/internal/GoogleAnalytics4.java b/src/main/java/net/openhft/chronicle/analytics/internal/GoogleAnalytics4.java index 9e7d210..1b96900 100644 --- a/src/main/java/net/openhft/chronicle/analytics/internal/GoogleAnalytics4.java +++ b/src/main/java/net/openhft/chronicle/analytics/internal/GoogleAnalytics4.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2020 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +15,6 @@ */ package net.openhft.chronicle.analytics.internal; -import net.openhft.chronicle.analytics.Analytics; import org.jetbrains.annotations.NotNull; import java.util.Map; @@ -28,7 +25,7 @@ import static net.openhft.chronicle.analytics.internal.JsonUtil.asElement; import static net.openhft.chronicle.analytics.internal.JsonUtil.jsonElement; -final class GoogleAnalytics4 extends AbstractGoogleAnalytics implements Analytics { +final class GoogleAnalytics4 extends AbstractGoogleAnalytics { GoogleAnalytics4(@NotNull final AnalyticsConfiguration configuration) { super(configuration); diff --git a/src/main/java/net/openhft/chronicle/analytics/internal/HttpUtil.java b/src/main/java/net/openhft/chronicle/analytics/internal/HttpUtil.java index 9ef3ea3..9564cee 100644 --- a/src/main/java/net/openhft/chronicle/analytics/internal/HttpUtil.java +++ b/src/main/java/net/openhft/chronicle/analytics/internal/HttpUtil.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2020 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,8 +76,7 @@ static final class Sender implements Runnable { @Override public void run() { try { - @SuppressWarnings("deprecation") - final URL url = new URL(urlString); + @SuppressWarnings("deprecation") final URL url = new URL(urlString); final HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // Do not linger if the connection is slow. Give up instead! conn.setConnectTimeout(DEFAULT_TIME_OUT_MS); diff --git a/src/main/java/net/openhft/chronicle/analytics/internal/InternalAnalyticsException.java b/src/main/java/net/openhft/chronicle/analytics/internal/InternalAnalyticsException.java index fdd4062..b91d87e 100644 --- a/src/main/java/net/openhft/chronicle/analytics/internal/InternalAnalyticsException.java +++ b/src/main/java/net/openhft/chronicle/analytics/internal/InternalAnalyticsException.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/net/openhft/chronicle/analytics/internal/JUnitUtil.java b/src/main/java/net/openhft/chronicle/analytics/internal/JUnitUtil.java index 7efabf2..02b6319 100644 --- a/src/main/java/net/openhft/chronicle/analytics/internal/JUnitUtil.java +++ b/src/main/java/net/openhft/chronicle/analytics/internal/JUnitUtil.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +22,8 @@ final class JUnitUtil { - private JUnitUtil() {} + private JUnitUtil() { + } static boolean isJUnitAvailable() { return Stream.of("org.junit.jupiter.api.Test", "org.junit.Test") diff --git a/src/main/java/net/openhft/chronicle/analytics/internal/JsonUtil.java b/src/main/java/net/openhft/chronicle/analytics/internal/JsonUtil.java index 1dd5929..eb9412f 100644 --- a/src/main/java/net/openhft/chronicle/analytics/internal/JsonUtil.java +++ b/src/main/java/net/openhft/chronicle/analytics/internal/JsonUtil.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/net/openhft/chronicle/analytics/internal/MuteAnalytics.java b/src/main/java/net/openhft/chronicle/analytics/internal/MuteAnalytics.java index 0130638..9810612 100644 --- a/src/main/java/net/openhft/chronicle/analytics/internal/MuteAnalytics.java +++ b/src/main/java/net/openhft/chronicle/analytics/internal/MuteAnalytics.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/net/openhft/chronicle/analytics/internal/VanillaAnalyticsBuilder.java b/src/main/java/net/openhft/chronicle/analytics/internal/VanillaAnalyticsBuilder.java index ce46afa..7b73835 100644 --- a/src/main/java/net/openhft/chronicle/analytics/internal/VanillaAnalyticsBuilder.java +++ b/src/main/java/net/openhft/chronicle/analytics/internal/VanillaAnalyticsBuilder.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2020 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -118,12 +116,11 @@ public Analytics build() { if (JUnitUtil.isJUnitAvailable() && !reportDespiteJUnit) return MuteAnalytics.INSTANCE; - else - if (measurementId.startsWith("UA-")) { - return new GoogleAnalytics3(this); - } else { - return new GoogleAnalytics4(this); - } + else if (measurementId.startsWith("UA-")) { + return new GoogleAnalytics3(this); + } else { + return new GoogleAnalytics4(this); + } } // Accessors @@ -140,14 +137,12 @@ public Analytics build() { @Override public @NotNull Map userProperties() { - // ok because the builder cannot be reused - return userProperties; + return new LinkedHashMap<>(userProperties); } @Override public @NotNull Map eventParameters() { - // ok because the builder cannot be reused - return eventParameters; + return new LinkedHashMap<>(eventParameters); } @SuppressWarnings("unchecked") @@ -163,7 +158,9 @@ public Analytics build() { } @Override - public int messages() {return messages; } + public int messages() { + return messages; + } @Override public long duration() { diff --git a/src/main/java/net/openhft/chronicle/analytics/internal/package-info.java b/src/main/java/net/openhft/chronicle/analytics/internal/package-info.java index 964b722..1ea90c1 100644 --- a/src/main/java/net/openhft/chronicle/analytics/internal/package-info.java +++ b/src/main/java/net/openhft/chronicle/analytics/internal/package-info.java @@ -2,8 +2,8 @@ * This package and any and all sub-packages contains strictly internal classes for this Chronicle library. * Internal classes shall never be used directly. *

- * Specifically, the following actions (including, but not limited to) are not allowed - * on internal classes and packages: + * Specifically, the following actions (including, but not limited to) are not allowed + * on internal classes and packages: *

    *
  • Casting to
  • *
  • Reflection of any kind
  • diff --git a/src/test/java/net/openhft/chronicle/analytics/AnalyticsExampleMain.java b/src/test/java/net/openhft/chronicle/analytics/AnalyticsExampleMain.java index 47f5ec6..2281737 100644 --- a/src/test/java/net/openhft/chronicle/analytics/AnalyticsExampleMain.java +++ b/src/test/java/net/openhft/chronicle/analytics/AnalyticsExampleMain.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2020 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/net/openhft/chronicle/analytics/AnalyticsTest.java b/src/test/java/net/openhft/chronicle/analytics/AnalyticsTest.java index 728e1a7..ac08319 100644 --- a/src/test/java/net/openhft/chronicle/analytics/AnalyticsTest.java +++ b/src/test/java/net/openhft/chronicle/analytics/AnalyticsTest.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +18,11 @@ import org.junit.jupiter.api.Test; +import java.util.Collections; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; class AnalyticsTest { @@ -32,15 +31,30 @@ class AnalyticsTest { @Test void sendEvent() { final AtomicReference sendName = new AtomicReference<>(); - final Analytics analytics = (name, additionalEventParameters) -> sendName.set(name); + final AtomicReference> sendParameters = new AtomicReference<>(); + final Analytics analytics = (name, additionalEventParameters) -> { + sendName.set(name); + sendParameters.set(additionalEventParameters); + }; analytics.sendEvent(TEST_STRING); assertEquals(TEST_STRING, sendName.get()); + assertEquals(Collections.emptyMap(), sendParameters.get()); + } + + @Test + void sendEventWithAdditionalParameters() { + final Map parameters = Collections.singletonMap("key", "value"); + final AtomicReference> capturedParameters = new AtomicReference<>(); + final Analytics analytics = (name, additionalEventParameters) -> capturedParameters.set(additionalEventParameters); + + analytics.sendEvent(TEST_STRING, parameters); + assertSame(parameters, capturedParameters.get()); } @Test void builder() { - Analytics.Builder builder= Analytics.builder(TEST_STRING, TEST_STRING); + Analytics.Builder builder = Analytics.builder(TEST_STRING, TEST_STRING); assertNotNull(builder); } } diff --git a/src/test/java/net/openhft/chronicle/analytics/GoogleAnalytics3Main.java b/src/test/java/net/openhft/chronicle/analytics/GoogleAnalytics3Main.java index 25c396f..0985a0b 100644 --- a/src/test/java/net/openhft/chronicle/analytics/GoogleAnalytics3Main.java +++ b/src/test/java/net/openhft/chronicle/analytics/GoogleAnalytics3Main.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/net/openhft/chronicle/analytics/internal/ClientIdUtilTest.java b/src/test/java/net/openhft/chronicle/analytics/internal/ClientIdUtilTest.java index 777cdb4..5bd56ba 100644 --- a/src/test/java/net/openhft/chronicle/analytics/internal/ClientIdUtilTest.java +++ b/src/test/java/net/openhft/chronicle/analytics/internal/ClientIdUtilTest.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +16,12 @@ package net.openhft.chronicle.analytics.internal; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; -import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -31,40 +30,38 @@ class ClientIdUtilTest { - private static final String FILE_NAME = "client.id"; + @TempDir + java.nio.file.Path tempDir; + + private java.nio.file.Path clientIdPath; private List debugMessages; @BeforeEach void beforeEach() { - cleanupFile(); + clientIdPath = tempDir.resolve("client.id"); debugMessages = new ArrayList<>(); } - @AfterEach - void afterEach() { - cleanupFile(); - } - @Test void acquireClientId() { // First time - final String clientId = FilesUtil.acquireClientId(FILE_NAME, debugMessages::add); + final String clientId = FilesUtil.acquireClientId(clientIdPath.toString(), debugMessages::add); assertDoesNotThrow(() -> UUID.fromString(clientId)); assertEquals(1, debugMessages.size()); final String msg = debugMessages.get(0); assertTrue(msg.contains("file not present")); - assertTrue(msg.contains(FILE_NAME)); + assertTrue(msg.contains(clientIdPath.toString())); // Second time should give the same id final List debugMessages2 = new ArrayList<>(); - final String clientId2 = FilesUtil.acquireClientId(FILE_NAME, debugMessages2::add); + final String clientId2 = FilesUtil.acquireClientId(clientIdPath.toString(), debugMessages2::add); assertEquals(clientId, clientId2); assertTrue(debugMessages2.isEmpty()); } @Test void acquireClientIdIllegalFile() { - final String illegalFileName = "."; + final String illegalFileName = tempDir.toString(); final String clientId = FilesUtil.acquireClientId(illegalFileName, debugMessages::add); assertNotNull(clientId); @@ -74,7 +71,29 @@ void acquireClientIdIllegalFile() { } - private void cleanupFile() { - new File(FILE_NAME).delete(); + @Test + void acquireClientIdAllowsParentSegments() throws Exception { + final String relativeName = "../client-id-parent-test"; + + final String originalUserDir = System.getProperty("user.dir"); + Path targetFile = null; + try { + assertNotNull(tempDir.getParent(), "Temp directory must have a parent for this test"); + System.setProperty("user.dir", tempDir.toString()); + final Path resolved = java.nio.file.Paths.get(relativeName).normalize(); + targetFile = resolved.isAbsolute() ? resolved : resolved.toAbsolutePath().normalize(); + Files.deleteIfExists(targetFile); + final List debugMessagesLocal = new ArrayList<>(); + final String clientId = FilesUtil.acquireClientId(relativeName, debugMessagesLocal::add); + assertDoesNotThrow(() -> UUID.fromString(clientId)); + assertTrue(Files.exists(targetFile)); + assertFalse(debugMessagesLocal.isEmpty()); + assertTrue(debugMessagesLocal.get(0).contains("file not present")); + } finally { + System.setProperty("user.dir", originalUserDir); + if (targetFile != null) { + Files.deleteIfExists(targetFile); + } + } } } diff --git a/src/test/java/net/openhft/chronicle/analytics/internal/FilesUtilTest.java b/src/test/java/net/openhft/chronicle/analytics/internal/FilesUtilTest.java new file mode 100644 index 0000000..039c75f --- /dev/null +++ b/src/test/java/net/openhft/chronicle/analytics/internal/FilesUtilTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.chronicle.analytics.internal; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class FilesUtilTest { + + @TempDir + Path tempDir; + + @Test + void touchLastContentHandlesMissingParent() { + final Path missingParentFile = tempDir.resolve("missing").resolve("chronicle-last.txt"); + assertDoesNotThrow(() -> FilesUtil.touchLastContent(missingParentFile)); + } + + @Test + void removeLastUsedFileTimeStampSecondHandlesDirectory() throws IOException { + final Path customHome = tempDir.resolve("home"); + Files.createDirectories(customHome); + final String originalUserHome = System.getProperty("user.home"); + try { + System.setProperty("user.home", customHome.toString()); + final Path directory = customHome.resolve(".chronicle.analytics.last"); + Files.createDirectories(directory.resolve("child")); + assertDoesNotThrow(FilesUtil::removeLastUsedFileTimeStampSecond); + assertNotNull(System.getProperty("user.home")); // ensure property remains accessible + } finally { + System.setProperty("user.home", originalUserHome); + } + } +} diff --git a/src/test/java/net/openhft/chronicle/analytics/internal/GoogleAnalytics3Test.java b/src/test/java/net/openhft/chronicle/analytics/internal/GoogleAnalytics3Test.java new file mode 100644 index 0000000..865ab6b --- /dev/null +++ b/src/test/java/net/openhft/chronicle/analytics/internal/GoogleAnalytics3Test.java @@ -0,0 +1,163 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.chronicle.analytics.internal; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import static net.openhft.chronicle.analytics.internal.FilesUtil.removeLastUsedFileTimeStampSecond; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class GoogleAnalytics3Test { + + @TempDir + Path tempDir; + + @Test + void bodyForIncludesBuilderAndAdditionalParameters() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + removeLastUsedFileTimeStampSecond(); + + final Map builderEventParameters = new LinkedHashMap<>(); + builderEventParameters.put("app_version", "9.9.9"); + builderEventParameters.put("builderKey", "builderValue"); + + final Map userProperties = new LinkedHashMap<>(); + userProperties.put("userKey", "userValue"); + + final TestAnalyticsConfiguration configuration = new TestAnalyticsConfiguration( + "UA-TEST-123", + "secret", + userProperties, + builderEventParameters, + tempDir.resolve("client.id").toString() + ); + + final GoogleAnalytics3 analytics = new GoogleAnalytics3(configuration); + + final Map merged = new LinkedHashMap<>(builderEventParameters); + merged.put("extraKey", "extraValue"); + + final Method bodyFor = GoogleAnalytics3.class.getDeclaredMethod( + "bodyFor", + String.class, + String.class, + Map.class, + Map.class + ); + bodyFor.setAccessible(true); + + final String payload = (String) bodyFor.invoke(analytics, "boot", "client-123", merged, userProperties); + + assertTrue(payload.contains("tid=UA-TEST-123")); + assertTrue(payload.contains("cid=client-123")); + assertTrue(payload.contains("cd=boot")); + assertTrue(payload.contains("an=secret")); + assertTrue(payload.contains("av=9.9.9")); + assertTrue(payload.contains("cd1=builderValue")); + assertTrue(payload.contains("cd2=extraValue")); + assertTrue(payload.contains("cd3=userValue")); + assertFalse(merged.containsKey("app_version")); + } + + private static final class TestAnalyticsConfiguration implements AnalyticsConfiguration { + + private final String measurementId; + private final String apiSecret; + private final Map userProperties; + private final Map eventParameters; + private final String clientIdFileName; + + private TestAnalyticsConfiguration(@NotNull final String measurementId, + @NotNull final String apiSecret, + @NotNull final Map userProperties, + @NotNull final Map eventParameters, + @NotNull final String clientIdFileName) { + this.measurementId = measurementId; + this.apiSecret = apiSecret; + this.userProperties = userProperties; + this.eventParameters = eventParameters; + this.clientIdFileName = clientIdFileName; + } + + @Override + public @NotNull String measurementId() { + return measurementId; + } + + @Override + public @NotNull String apiSecret() { + return apiSecret; + } + + @Override + public @NotNull Map userProperties() { + return userProperties; + } + + @Override + public @NotNull Map eventParameters() { + return eventParameters; + } + + @Override + public @NotNull Consumer errorLogger() { + return s -> { + }; + } + + @Override + public @NotNull Consumer debugLogger() { + return s -> { + }; + } + + @Override + public long duration() { + return 0; + } + + @Override + public int messages() { + return 0; + } + + @Override + public @NotNull TimeUnit timeUnit() { + return TimeUnit.SECONDS; + } + + @Override + public @NotNull String clientIdFileName() { + return clientIdFileName; + } + + @Override + public @NotNull String url() { + return "https://example.invalid"; + } + } +} diff --git a/src/test/java/net/openhft/chronicle/analytics/internal/GoogleAnalyticsTest.java b/src/test/java/net/openhft/chronicle/analytics/internal/GoogleAnalyticsTest.java index 136dd26..7ce86c3 100644 --- a/src/test/java/net/openhft/chronicle/analytics/internal/GoogleAnalyticsTest.java +++ b/src/test/java/net/openhft/chronicle/analytics/internal/GoogleAnalyticsTest.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/net/openhft/chronicle/analytics/internal/HttpUtilTest.java b/src/test/java/net/openhft/chronicle/analytics/internal/HttpUtilTest.java index b743340..49aee0c 100644 --- a/src/test/java/net/openhft/chronicle/analytics/internal/HttpUtilTest.java +++ b/src/test/java/net/openhft/chronicle/analytics/internal/HttpUtilTest.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,6 +78,29 @@ void sendIllegalURL() { assertTrue(debugResponses.isEmpty()); } + @Test + void sendUsesBackgroundExecutor() throws IOException, InterruptedException { + final MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody(TEST_RESPONSE)); + + final CountDownLatch latch = new CountDownLatch(1); + + server.start(); + try { + final HttpUrl url = server.url("mp/collect"); + HttpUtil.send(url.url().toString(), "{}", errorResponses::add, msg -> { + debugResponses.add(msg); + latch.countDown(); + }); + + assertTrue(latch.await(2, TimeUnit.SECONDS)); + assertTrue(errorResponses.isEmpty()); + assertEquals(singletonList(TEST_RESPONSE.replaceAll("\\s+", " ").trim()), debugResponses); + } finally { + server.shutdown(); + } + } + @Test void urlEncode() { final List logMessages = new ArrayList<>(); @@ -120,7 +141,7 @@ void hungHttpServer() throws IOException { @Override public @NotNull MockResponse dispatch(@NotNull RecordedRequest recordedRequest) { int cnt = 0; - for (int i = 0; i < delayMs/latchPollMs; i++) { + for (int i = 0; i < delayMs / latchPollMs; i++) { try { if (countDownLatch.await(latchPollMs, TimeUnit.MILLISECONDS)) break; diff --git a/src/test/java/net/openhft/chronicle/analytics/internal/InternalAnalyticsExceptionTest.java b/src/test/java/net/openhft/chronicle/analytics/internal/InternalAnalyticsExceptionTest.java index 74312a4..61fdc75 100644 --- a/src/test/java/net/openhft/chronicle/analytics/internal/InternalAnalyticsExceptionTest.java +++ b/src/test/java/net/openhft/chronicle/analytics/internal/InternalAnalyticsExceptionTest.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/net/openhft/chronicle/analytics/internal/JUnitUtilTest.java b/src/test/java/net/openhft/chronicle/analytics/internal/JUnitUtilTest.java index 9e070c0..efffc48 100644 --- a/src/test/java/net/openhft/chronicle/analytics/internal/JUnitUtilTest.java +++ b/src/test/java/net/openhft/chronicle/analytics/internal/JUnitUtilTest.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +30,7 @@ void isJUnitAvailable() { @Test void isClassAvailableString() { - assertTrue(JUnitUtil.isClassAvailable(String.class.getName())); + assertTrue(JUnitUtil.isClassAvailable(String.class.getName())); } @Test diff --git a/src/test/java/net/openhft/chronicle/analytics/internal/MuteAnalyticsTest.java b/src/test/java/net/openhft/chronicle/analytics/internal/MuteAnalyticsTest.java index 303b40d..4eec05c 100644 --- a/src/test/java/net/openhft/chronicle/analytics/internal/MuteAnalyticsTest.java +++ b/src/test/java/net/openhft/chronicle/analytics/internal/MuteAnalyticsTest.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/net/openhft/chronicle/analytics/internal/VanillaAnalyticsBuilderTest.java b/src/test/java/net/openhft/chronicle/analytics/internal/VanillaAnalyticsBuilderTest.java index 3ea98c5..864a629 100644 --- a/src/test/java/net/openhft/chronicle/analytics/internal/VanillaAnalyticsBuilderTest.java +++ b/src/test/java/net/openhft/chronicle/analytics/internal/VanillaAnalyticsBuilderTest.java @@ -1,7 +1,5 @@ /* - * Copyright 2016-2022 chronicle.software - * - * https://chronicle.software + * Copyright 2016-2025 chronicle.software * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.