Skip to content
Merged
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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

# CodeClocker Changelog

## [1.3.0] - 2025-12-15

- Save activity data to local storage to survive IDE restarts

## [1.2.3] - 2025-11-29

- Reset VCS changes at midnight in IDE status bar
Expand Down Expand Up @@ -72,7 +76,8 @@

- Support IntelliJ Platform 2024.3.5

[Unreleased]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.2.2...HEAD
[Unreleased]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.2.3...HEAD
[1.2.3]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.2.2...v1.2.3
[1.2.2]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.2.1...v1.2.2
[1.2.1]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.2.0...v1.2.1
[1.2.0]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.1.0...v1.2.0
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pluginGroup = com.codeclocker
pluginName = CodeClocker
pluginRepositoryUrl = https://github.com/codeclocker/codeclocker-intellij-plugin
# SemVer format -> https://semver.org
pluginVersion = 1.2.3
pluginVersion = 1.3.0

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 242
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import static com.codeclocker.plugin.intellij.widget.TimeTrackerInitializer.initializeTimerWidgets;
import static java.awt.AWTEvent.FOCUS_EVENT_MASK;

import com.codeclocker.plugin.intellij.analytics.AnalyticsReportingTask;
import com.codeclocker.plugin.intellij.apikey.ApiKeyPromptStartupActivity;
import com.codeclocker.plugin.intellij.listeners.FocusListener;
import com.codeclocker.plugin.intellij.reporting.DataReportingTask;
import com.codeclocker.plugin.intellij.reporting.TimeComparisonFetchTask;
import com.codeclocker.plugin.intellij.subscription.SubscriptionStateCheckerTask;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
Expand Down Expand Up @@ -34,6 +36,8 @@ public Object execute(
registerFocusListener();
startDataReportingTask();
startCheckingApiKeyStatus();
startTimeComparisonFetchTask();
startAnalyticsReportingTask();
ApiKeyPromptStartupActivity.showApiKeyDialog();
initializeTimerWidgets();

Expand All @@ -48,11 +52,18 @@ private static void registerFocusListener() {
}

private static void startDataReportingTask() {
DataReportingTask task = new DataReportingTask();
task.schedule();
ApplicationManager.getApplication().getService(DataReportingTask.class).schedule();
}

private static void startCheckingApiKeyStatus() {
ApplicationManager.getApplication().getService(SubscriptionStateCheckerTask.class).schedule();
}

private static void startTimeComparisonFetchTask() {
ApplicationManager.getApplication().getService(TimeComparisonFetchTask.class).schedule();
}

private static void startAnalyticsReportingTask() {
ApplicationManager.getApplication().getService(AnalyticsReportingTask.class).schedule();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.codeclocker.plugin.intellij.analytics;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import java.util.Map;
import org.jetbrains.annotations.NotNull;

/**
* Simple facade for tracking analytics events from anywhere in the plugin. Usage:
*
* <pre>
* Analytics.track(AnalyticsEventType.WIDGET_CLICK);
* Analytics.track(AnalyticsEventType.WIDGET_POPUP_ACTION, Map.of("action", "pause"));
* </pre>
*/
public final class Analytics {

private static final Logger LOG = Logger.getInstance(Analytics.class);

private Analytics() {}

/**
* Tracks an analytics event.
*
* @param eventType The type of event to track (use constants from {@link AnalyticsEventType})
*/
public static void track(@NotNull String eventType) {
track(eventType, Map.of());
}

/**
* Tracks an analytics event with properties.
*
* @param eventType The type of event to track
* @param properties Additional event-specific properties
*/
public static void track(@NotNull String eventType, @NotNull Map<String, Object> properties) {
try {
AnalyticsReportingTask task =
ApplicationManager.getApplication().getService(AnalyticsReportingTask.class);
if (task != null) {
task.track(eventType, properties);
} else {
LOG.debug("AnalyticsReportingTask not available, event not tracked: " + eventType);
}
} catch (Exception ex) {
LOG.debug("Failed to track event: " + eventType, ex);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.codeclocker.plugin.intellij.analytics;

import java.util.Map;

/**
* Represents a single analytics event.
*
* @param eventType The type of event (use constants from {@link AnalyticsEventType})
* @param timestamp Unix timestamp in milliseconds when the event occurred
* @param properties Additional event-specific properties
*/
public record AnalyticsEvent(String eventType, long timestamp, Map<String, Object> properties) {

public static AnalyticsEvent of(String eventType) {
return new AnalyticsEvent(eventType, System.currentTimeMillis(), Map.of());
}

public static AnalyticsEvent of(String eventType, Map<String, Object> properties) {
return new AnalyticsEvent(eventType, System.currentTimeMillis(), properties);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.codeclocker.plugin.intellij.analytics;

/** Constants for analytics event types. Using constants instead of enum for extensibility. */
public final class AnalyticsEventType {

private AnalyticsEventType() {}

// Widget events
public static final String STATUS_BAR_WIDGET_CLICK = "status_bar_widget_click";
public static final String WIDGET_POPUP_OPEN = "widget_popup_open";
public static final String WIDGET_POPUP_ACTION = "widget_popup_action";

// API key events
public static final String API_KEY_ENTERED = "api_key_entered";
public static final String API_KEY_CLEARED = "api_key_cleared";
public static final String API_KEY_DIALOG_OPENED = "api_key_dialog_opened";
public static final String API_KEY_DIALOG_CANCELLED = "api_key_dialog_cancelled";

// Plugin lifecycle events
public static final String PLUGIN_STARTED = "plugin_started";
public static final String PLUGIN_STOPPED = "plugin_stopped";

// Action events
public static final String ACTION_SHOW_STATISTICS = "action_show_statistics";

// Feature usage events
public static final String FEATURE_LOCAL_STORAGE_SYNC = "feature_local_storage_sync";

// Error events
public static final String ERROR_DATA_REPORT_FAILED = "error_data_report_failed";
public static final String ERROR_API_REQUEST_FAILED = "error_api_request_failed";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.codeclocker.plugin.intellij.analytics;

import static com.codeclocker.plugin.intellij.HubHost.HUB_API_HOST;
import static com.codeclocker.plugin.intellij.JsonMapper.OBJECT_MAPPER;
import static com.codeclocker.plugin.intellij.Timeouts.CONNECT_TIMEOUT;
import static com.codeclocker.plugin.intellij.Timeouts.READ_TIMEOUT;
import static com.intellij.util.io.HttpRequests.JSON_CONTENT_TYPE;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.codeclocker.plugin.intellij.apikey.ApiKeyLifecycle;
import com.codeclocker.plugin.intellij.reporting.SentStatus;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.util.io.HttpRequests;
import java.io.IOException;

/** HTTP client for sending analytics reports to the backend. */
public class AnalyticsHttpClient {

private static final Logger LOG = Logger.getInstance(AnalyticsHttpClient.class);

private static final String ANALYTICS_ENDPOINT = "/api/v1/analytics/events";

/**
* Sends an analytics report to the backend.
*
* @param report The analytics report to send
* @return SentStatus indicating success or failure
*/
public SentStatus sendAnalyticsReport(AnalyticsReportDto report) {
try {
String jsonData = OBJECT_MAPPER.writeValueAsString(report);
LOG.debug("Sending analytics report with " + report.events().size() + " events");

String apiKey = ApiKeyLifecycle.getActiveApiKey();

HttpRequests.post(HUB_API_HOST + ANALYTICS_ENDPOINT, JSON_CONTENT_TYPE)
.connectTimeout(CONNECT_TIMEOUT)
.readTimeout(READ_TIMEOUT)
.tuner(
connection -> {
if (apiKey != null && !apiKey.isBlank()) {
connection.setRequestProperty("X-codeclocker-api-key", apiKey);
}
})
.connect(
request -> {
request.write(jsonData.getBytes(UTF_8));
return request.readString();
});

LOG.debug("Analytics report sent successfully");
return SentStatus.OK;
} catch (JsonProcessingException ex) {
LOG.warn("Failed to serialize analytics report: " + ex.getMessage());
return SentStatus.ERROR;
} catch (IOException ex) {
LOG.debug("Failed to send analytics report: " + ex.getMessage());
return SentStatus.ERROR;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.codeclocker.plugin.intellij.analytics;

import com.intellij.openapi.diagnostic.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
* Thread-safe queue for collecting analytics events. Events are accumulated and can be drained for
* batch reporting.
*/
public class AnalyticsQueue {

private static final Logger LOG = Logger.getInstance(AnalyticsQueue.class);

private static final int MAX_QUEUE_SIZE = 1000;

private final ConcurrentLinkedQueue<AnalyticsEvent> events = new ConcurrentLinkedQueue<>();

/**
* Tracks an analytics event.
*
* @param eventType The type of event to track
*/
public void track(String eventType) {
track(eventType, Map.of());
}

/**
* Tracks an analytics event with properties.
*
* @param eventType The type of event to track
* @param properties Additional event properties
*/
public void track(String eventType, Map<String, Object> properties) {
if (events.size() >= MAX_QUEUE_SIZE) {
LOG.warn("Analytics queue is full, dropping oldest event");
events.poll();
}

AnalyticsEvent event = AnalyticsEvent.of(eventType, properties);
events.add(event);
LOG.debug("Tracked analytics event: " + eventType);
}

/**
* Drains all events from the queue.
*
* @return List of all queued events (queue is emptied)
*/
public List<AnalyticsEvent> drain() {
List<AnalyticsEvent> drained = new ArrayList<>();
AnalyticsEvent event;
while ((event = events.poll()) != null) {
drained.add(event);
}
return drained;
}

/** Returns the number of events in the queue. */
public int size() {
return events.size();
}

/** Checks if the queue is empty. */
public boolean isEmpty() {
return events.isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.codeclocker.plugin.intellij.analytics;

import java.util.List;
import java.util.Map;

/** DTO for sending analytics report to the backend. */
public record AnalyticsReportDto(
String installationId,
IdeContextDto ideContext,
List<AnalyticsEventDto> events,
long reportedAt) {

public static AnalyticsReportDto from(
String installationId, IdeContext context, List<AnalyticsEvent> events) {
return new AnalyticsReportDto(
installationId,
IdeContextDto.from(context),
events.stream().map(AnalyticsEventDto::from).toList(),
System.currentTimeMillis());
}

public record IdeContextDto(
String ideName,
String ideVersion,
String ideBuild,
String ideProductCode,
String pluginVersion,
String osName,
String osVersion,
String osArch,
String javaVersion,
String locale) {
public static IdeContextDto from(IdeContext context) {
return new IdeContextDto(
context.ideName(),
context.ideVersion(),
context.ideBuild(),
context.ideProductCode(),
context.pluginVersion(),
context.osName(),
context.osVersion(),
context.osArch(),
context.javaVersion(),
context.locale());
}
}

public record AnalyticsEventDto(
String eventType, long timestamp, Map<String, Object> properties) {
public static AnalyticsEventDto from(AnalyticsEvent event) {
return new AnalyticsEventDto(event.eventType(), event.timestamp(), event.properties());
}
}
}
Loading
Loading