Skip to content

Commit

Permalink
Add daily limits support for events (1 startup/day)
Browse files Browse the repository at this point in the history
Signed-off-by: Fred Bricon <[email protected]>
  • Loading branch information
fbricon committed May 23, 2024
1 parent 82034e5 commit d5737dc
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 17 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ Starting with 0.6.1, you can configure ratios on included events, meaning X% of
"refresh": "12h",
"includes": [
{
"name" : "*"
"name" : "startup",
"dailyLimit": "1" // Limit to 1 event per day per extension
},
{
"name" : "*" // Always put wildcard patterns last in the array, to ensure other events are included
}
]
},
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@redhat-developer/vscode-redhat-telemetry",
"version": "0.7.1",
"version": "0.8.0",
"description": "Provides Telemetry APIs for Red Hat applications",
"main": "lib/index.js",
"types": "lib",
Expand Down
33 changes: 27 additions & 6 deletions src/common/impl/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import * as picomatch from "picomatch";
import { isError } from "../utils/events";
import { numValue } from "../utils/hashcode";
import { AnalyticsEvent } from "../api/analyticsEvent";
import { Logger } from "../utils/logger";

interface EventNamePattern {
name: string;
ratio?: string;
dailyLimit?: string;
}

interface PropertyPattern {
Expand Down Expand Up @@ -43,11 +43,27 @@ export class Configuration {
return false;
}

const isIncluded = this.isIncluded(event, currUserRatioValue) && !this.isExcluded(event, currUserRatioValue);

const isIncluded = this.isIncluded(event, currUserRatioValue)
&& !this.isExcluded(event, currUserRatioValue)
return isIncluded;
}

public getDailyLimit(event: AnalyticsEvent): number {
const includes = this.getIncludePatterns();
if (includes.length) {
const pattern = includes.filter(isEventNamePattern).map(p => p as EventNamePattern)
.find(p => picomatch.isMatch(event.event, p.name))
if (pattern?.dailyLimit) {
try {
return parseInt(pattern.dailyLimit);
} catch(e) {
// ignore
}
}
}
return Number.MAX_VALUE;
}

isIncluded(event: AnalyticsEvent, currUserRatioValue: number): boolean {
const includes = this.getIncludePatterns();
if (includes.length) {
Expand All @@ -72,7 +88,7 @@ export class Configuration {
}

getExcludePatterns(): EventPattern[] {
if (this.json.excludes) {
if (this.json?.excludes) {
return this.json.excludes as EventPattern[];
}
return [];
Expand Down Expand Up @@ -121,11 +137,16 @@ function getRatio(ratioAsString ?:string): number {
return 1.0;
}



function isPropertyPattern(event: EventPattern): event is PropertyPattern {
if ((event as PropertyPattern).property) {
return true
}
return false
}

function isEventNamePattern(event: EventPattern): event is EventNamePattern {
if ((event as EventNamePattern).name) {
return true
}
return false
}
49 changes: 49 additions & 0 deletions src/common/impl/eventTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Memento } from "vscode";
import { AnalyticsEvent } from '../api/analyticsEvent';


interface EventTracking {
lastUpdated: number;
count: number;
}

export class EventTracker {

constructor(private globalState: Memento) { }

async storeEventCount(payload: AnalyticsEvent, newCount: number): Promise<void> {
const newTracking = {
count: newCount,
lastUpdated: this.getTodaysTimestamp()
} as EventTracking;
return this.globalState.update(this.getEventTrackingKey(payload.event), newTracking);
}

async getEventCount(payload: AnalyticsEvent): Promise<number> {
const eventTracking = this.globalState.get<EventTracking>(this.getEventTrackingKey(payload.event));
if (eventTracking) {
//Check if eventTracking timestamp is older than today
let today = this.getTodaysTimestamp();
let lastEventDay = eventTracking.lastUpdated;
//check if now and lastEventTime are in the same day
if (lastEventDay === today) {
return eventTracking.count;
}
// new day, reset count
}
return 0;
}

private getTodaysTimestamp(): number {
const now = new Date();
now.setHours(0, 0, 0, 0);
return now.getTime();
}

private getEventTrackingKey(eventName: string): string {
//replace all non alphanumeric characters with a _
const key = eventName.replace(/[^a-zA-Z0-9]/g, '_');
return `telemetry.events.tracking.${key}`;
}
}

33 changes: 28 additions & 5 deletions src/common/impl/telemetryServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,26 @@ import { IdProvider } from '../api/idProvider';
import { Environment } from '../api/environment';
import { transform, isError } from '../utils/events';
import { IReporter } from '../api/reporter';
import { EventTracker } from './eventTracker';
import { Memento } from 'vscode';

/**
* Implementation of a `TelemetryService`
*/
export class TelemetryServiceImpl implements TelemetryService {
private startTime: number;
private eventTracker: EventTracker;

constructor(private reporter: IReporter,
constructor(globalState: Memento,
private reporter: IReporter,
private queue: TelemetryEventQueue | undefined,
private settings: TelemetrySettings,
private idManager: IdProvider,
private environment: Environment,
private configurationManager?: ConfigurationManager) {
private configurationManager?: ConfigurationManager
) {
this.startTime = this.getCurrentTimeInSeconds();
this.eventTracker = new EventTracker(globalState);
}

/*
Expand Down Expand Up @@ -64,7 +70,25 @@ export class TelemetryServiceImpl implements TelemetryService {
//Check against Extension configuration
const config = await this.configurationManager?.getExtensionConfiguration();
if (!config || config.canSend(payload)) {
return this.reporter.report(payload);

const dailyLimit = (config)?config.getDailyLimit(payload):Number.MAX_VALUE;
let count = 0;
if (dailyLimit < Number.MAX_VALUE) {
//find currently stored count
count = await this.eventTracker.getEventCount(payload);
if (count >= dailyLimit){
//daily limit reached, do not send event
Logger.log(`Daily limit reached for ${event.name}: ${dailyLimit}`);
return;
}
}
return this.reporter.report(payload).then(()=>{
if (dailyLimit < Number.MAX_VALUE) {
//update count
Logger.log(`Storing event count (${count+1}/${dailyLimit}) for ${event.name}`);
return this.eventTracker.storeEventCount(payload, count+1);
}
});
}
}

Expand All @@ -87,9 +111,8 @@ export class TelemetryServiceImpl implements TelemetryService {
return this.reporter.closeAndFlush();
}


private getCurrentTimeInSeconds(): number {
const now = Date.now();
return Math.floor(now/1000);
}
}
}
14 changes: 11 additions & 3 deletions src/common/telemetryServiceBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import { TelemetryService } from './api/telemetry';
import { TelemetryServiceImpl } from './impl/telemetryServiceImpl';
import { TelemetryEventQueue } from './impl/telemetryEventQueue';
import { TelemetrySettings } from './api/settings';
import { getSegmentKey } from './utils/keyLocator';
import { getExtensionId } from './utils/extensions';
import { CacheService } from './api/cacheService';
import { ConfigurationManager } from './impl/configurationManager';
import { ExtensionContext } from 'vscode';

/**
* `TelemetryService` builder
Expand All @@ -20,6 +19,7 @@ export class TelemetryServiceBuilder {
private environment?: Environment;
private configurationManager?: ConfigurationManager;
private reporter?: IReporter;
private context?:ExtensionContext;

constructor(packageJson?: any) {
this.packageJson = packageJson;
Expand Down Expand Up @@ -55,6 +55,11 @@ export class TelemetryServiceBuilder {
return this;
}

public setContext(context: ExtensionContext): TelemetryServiceBuilder {
this.context = context;
return this;
}

public async build(): Promise<TelemetryService> {
this.validate();
if (!this.environment) {
Expand All @@ -76,10 +81,13 @@ export class TelemetryServiceBuilder {
const queue = this.settings!.isTelemetryConfigured()
? undefined
: new TelemetryEventQueue();
return new TelemetryServiceImpl(this.reporter!, queue, this.settings!, this.idProvider!, this.environment!, this.configurationManager);
return new TelemetryServiceImpl(this.context?.globalState!, this.reporter!, queue, this.settings!, this.idProvider!, this.environment!, this.configurationManager);
}

private validate() {
if (!this.context) {
throw new Error('context is not set');
}
if (!this.idProvider) {
throw new Error('idProvider is not set');
}
Expand Down
13 changes: 13 additions & 0 deletions src/config/telemetry-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"enabled":"all",
"refresh": "12h",
"includes": [
{
"name" : "startup",
"dailyLimit": 1
},
{
"name" : "*"
}
Expand All @@ -17,6 +21,15 @@
"redhat.java": {
"enabled": "all",
"ratio": "0.5",
"includes": [
{
"name" : "startup",
"dailyLimit": 1000
},
{
"name" : "*"
}
],
"excludes": [
{
"name": "textCompletion",
Expand Down
1 change: 1 addition & 0 deletions src/node/redHatServiceNodeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class RedHatServiceNodeProvider extends AbstractRedHatServiceProvider {
const reporter = new Reporter(this.getSegmentApi(packageJson), new EventCacheService(storageService));
const idManager = IdManagerFactory.getIdManager();
const builder = new TelemetryServiceBuilder(packageJson)
.setContext(this.context)
.setSettings(this.settings)
.setIdProvider(idManager)
.setReporter(reporter)
Expand Down
1 change: 1 addition & 0 deletions src/webworker/redHatServiceWebWorkerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class RedHatServiceWebWorkerProvider extends AbstractRedHatServiceProvide
const reporter = new Reporter(this.getSegmentApi(packageJson), new EventCacheService(storageService));
const idManager = new VFSSystemIdProvider(storageService);
const builder = new TelemetryServiceBuilder(packageJson)
.setContext(this.context)
.setSettings(this.settings)
.setIdProvider(idManager)
.setReporter(reporter)
Expand Down

0 comments on commit d5737dc

Please sign in to comment.