Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.{kt,kts}]
indent_size = 4
max_line_length = 140
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ktlint_standard_no-wildcard-imports = enabled

[*.{yml,yaml}]
indent_size = 2

[*.md]
trim_trailing_whitespace = false
19 changes: 19 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env sh
# Pre-commit hook: runs ktlintCheck if any Kotlin files are staged.
# Activated automatically by Gradle (sets core.hooksPath=.githooks on build).

set -e

staged=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(kt|kts)$' || true)

if [ -z "$staged" ]; then
exit 0
fi

echo "Running ktlintCheck on staged Kotlin files..."
if ! ./gradlew --quiet ktlintCheck; then
echo ""
echo "ktlint violations found."
echo "Run './gradlew ktlintFormat' to auto-fix, then re-stage and commit."
exit 1
fi
28 changes: 28 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
version: 2
updates:
# Gradle dependencies (build.gradle.kts) and Gradle plugins.
# Dependabot's gradle ecosystem excludes pre-release qualifiers
# (-rc, -alpha, -beta, -SNAPSHOT, -M*) by default, so only stable
# releases will be proposed.
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "gradle"
commit-message:
prefix: "deps"

# GitHub Actions used in workflows (.github/workflows/*).
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "ci"
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
build:
name: Lint and test (JDK 21)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@v4

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

- name: Ktlint
run: ./gradlew ktlintCheck

- name: Test
run: ./gradlew test

- name: Upload test report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-report
path: build/reports/tests/test
retention-days: 7
34 changes: 34 additions & 0 deletions .github/workflows/gradle-wrapper.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Update Gradle Wrapper

on:
schedule:
# First day of every month at 06:00 UTC.
- cron: '0 6 1 * *'
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
update-wrapper:
name: Bump Gradle wrapper if outdated
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

- name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
target-branch: main
labels: dependencies, gradle-wrapper
# Only propose stable Gradle releases.
release-channel: stable
40 changes: 38 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
kotlin("jvm") version "2.3.10"
id("com.vanniktech.maven.publish") version "0.36.0"
kotlin("plugin.serialization") version "2.3.10"
id("org.jlleitschuh.gradle.ktlint") version "14.2.0"
}

group = "io.github.criticalay"
Expand Down Expand Up @@ -49,7 +50,6 @@ dependencies {
implementation("io.github.oshai:kotlin-logging-jvm:6.0.9")
implementation("org.slf4j:slf4j-api:2.0.13")


testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testImplementation("io.mockk:mockk:1.13.8")
Expand All @@ -61,4 +61,40 @@ kotlin {

tasks.test {
useJUnitPlatform()
}
}

ktlint {
verbose.set(true)
android.set(false)
outputToConsole.set(true)
filter {
exclude("**/build/**")
}
}

val installGitHooks by tasks.registering {
description = "Configures git core.hooksPath to .githooks (enables ktlint pre-commit hook)."
group = "build setup"
onlyIf { file(".git").isDirectory && System.getenv("CI") == null }
doLast {
val current = ProcessBuilder("git", "config", "--get", "core.hooksPath")
.redirectErrorStream(true)
.start()
.let { p ->
val out = p.inputStream.bufferedReader().readText().trim()
p.waitFor()
out
}
if (current != ".githooks") {
ProcessBuilder("git", "config", "core.hooksPath", ".githooks")
.inheritIO()
.start()
.waitFor()
logger.lifecycle("Configured git core.hooksPath to .githooks (ktlint pre-commit hook active)")
}
}
}

tasks.named("ktlintCheck") {
dependsOn(installGitHooks)
}
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
rootProject.name = "google-analytics-kt"
rootProject.name = "google-analytics-kt"
12 changes: 7 additions & 5 deletions src/main/kotlin/GoogleAnalytics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ import com.criticalay.response.GaResponse
* Event delivery depends on [inSample] and configuration in [config].
*/
interface GoogleAnalytics : AutoCloseable {

/** The active configuration. */
val config: GoogleAnalyticsConfig

/** True if this instance was elected to be in-sample and will actually send events. */
val inSample: Boolean

// Hit-type factory methods

/**
* Creates a [PageViewHit] builder pre-configured for [clientId].
* Call [PageViewHit.send] (or [PageViewHit.sendAsync]) when ready.
Expand All @@ -66,7 +66,10 @@ interface GoogleAnalytics : AutoCloseable {
* Creates a [CustomHit] builder for an arbitrary GA4 event name.
* The [name] must follow GA4 naming rules (letters, digits, underscores, max 40 chars).
*/
fun custom(clientId: String, name: String): CustomHit
fun custom(
clientId: String,
name: String,
): CustomHit

/**
* Sends a fully constructed [GaRequest] synchronously.
Expand Down Expand Up @@ -102,7 +105,6 @@ interface GoogleAnalytics : AutoCloseable {
* }
* ```
*/
fun builder(block: GoogleAnalyticsBuilder.() -> Unit): GoogleAnalytics =
GoogleAnalyticsBuilder().apply(block).build()
fun builder(block: GoogleAnalyticsBuilder.() -> Unit): GoogleAnalytics = GoogleAnalyticsBuilder().apply(block).build()
}
}
}
36 changes: 18 additions & 18 deletions src/main/kotlin/GoogleAnalyticsBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

package com.criticalay

import com.criticalay.internal.GaImpl
import com.criticalay.httpclient.OkHttpClientImpl
import com.criticalay.internal.GaImpl

/**
* Builder for configuring and creating a [GoogleAnalytics] instance.
Expand Down Expand Up @@ -51,7 +51,6 @@ import com.criticalay.httpclient.OkHttpClientImpl
* Call [build] to create a fully configured [GoogleAnalytics] instance.
*/
class GoogleAnalyticsBuilder {

/** GA4 Measurement ID (e.g. "G-XXXXXXXX"). **Required.** */
var measurementId: String = ""

Expand Down Expand Up @@ -106,22 +105,23 @@ class GoogleAnalyticsBuilder {

/** Builds and returns a fully initialized [GoogleAnalytics] instance. */
fun build(): GoogleAnalytics {
val config = GoogleAnalyticsConfig(
measurementId = measurementId,
apiSecret = apiSecret,
appName = appName,
appVersion = appVersion,
enabled = enabled,
debug = debug,
samplePercentage = samplePercentage,
batchingEnabled = batchingEnabled,
batchSize = batchSize,
endpointUrl = endpointUrl,
proxyHost = proxyHost,
proxyPort = proxyPort,
connectTimeoutMs = connectTimeoutMs,
readTimeoutMs = readTimeoutMs,
)
val config =
GoogleAnalyticsConfig(
measurementId = measurementId,
apiSecret = apiSecret,
appName = appName,
appVersion = appVersion,
enabled = enabled,
debug = debug,
samplePercentage = samplePercentage,
batchingEnabled = batchingEnabled,
batchSize = batchSize,
endpointUrl = endpointUrl,
proxyHost = proxyHost,
proxyPort = proxyPort,
connectTimeoutMs = connectTimeoutMs,
readTimeoutMs = readTimeoutMs,
)
val httpClient = OkHttpClientImpl(config)
return GaImpl(config, httpClient)
}
Expand Down
7 changes: 3 additions & 4 deletions src/main/kotlin/GoogleAnalyticsConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.criticalay


/**
* Configuration for the GA4 Measurement Protocol SDK.
*
Expand Down Expand Up @@ -59,16 +58,16 @@ data class GoogleAnalyticsConfig(
) {
companion object {
const val DEFAULT_ENDPOINT = "https://www.google-analytics.com/mp/collect"
const val DEBUG_ENDPOINT = "https://www.google-analytics.com/debug/mp/collect"
const val DEBUG_ENDPOINT = "https://www.google-analytics.com/debug/mp/collect"
}

/** Returns the effective endpoint URL (debug or standard). */
fun effectiveEndpointUrl(): String = if (debug) DEBUG_ENDPOINT else endpointUrl

init {
require(measurementId.isNotBlank()) { "measurementId must not be blank" }
require(apiSecret.isNotBlank()) { "apiSecret must not be blank" }
require(apiSecret.isNotBlank()) { "apiSecret must not be blank" }
require(samplePercentage in 1..100) { "samplePercentage must be between 1 and 100" }
require(batchSize in 1..25) { "batchSize must be between 1 and 25 (GA4 limit)" }
require(batchSize in 1..25) { "batchSize must be between 1 and 25 (GA4 limit)" }
}
}
Loading
Loading