Browse Source

Refines telemetry

main
Eric Amodio 2 years ago
parent
commit
38a856c1be
9 changed files with 196 additions and 89 deletions
  1. +24
    -13
      src/extension.ts
  2. +81
    -30
      src/git/gitProviderService.ts
  3. +9
    -0
      src/git/models/repository.ts
  4. +17
    -5
      src/system/array.ts
  5. +2
    -4
      src/system/iterable.ts
  6. +6
    -2
      src/system/object.ts
  7. +9
    -5
      src/system/stopwatch.ts
  8. +6
    -6
      src/telemetry/openTelemetryProvider.ts
  9. +42
    -24
      src/telemetry/telemetry.ts

+ 24
- 13
src/extension.ts View File

@ -97,7 +97,10 @@ export async function activate(context: ExtensionContext): Promise
if (!workspace.isTrusted) {
void setContext(ContextKeys.Untrusted, true);
context.subscriptions.push(
workspace.onDidGrantWorkspaceTrust(() => void setContext(ContextKeys.Untrusted, undefined)),
workspace.onDidGrantWorkspaceTrust(() => {
void setContext(ContextKeys.Untrusted, undefined);
container.telemetry.setGlobalAttribute('workspace.isTrusted', workspace.isTrusted);
}),
);
}
@ -176,22 +179,30 @@ export async function activate(context: ExtensionContext): Promise
void setContext(ContextKeys.Debugging, true);
}
container.telemetry.setGlobalAttributes({
debugging: container.debugging,
insiders: insiders,
prerelease: prerelease,
install: previousVersion == null,
upgrade: previousVersion != null && gitlensVersion !== previousVersion,
upgradedFrom: previousVersion != null && gitlensVersion !== previousVersion ? previousVersion : undefined,
});
const mode = container.mode;
const data = {
'activation.elapsed': sw.elapsed(),
'activation.previousVersion': previousVersion,
'workspace.isTrusted': workspace.isTrusted,
};
const elapsed = sw.elapsed();
queueMicrotask(() => {
container.telemetry.setGlobalAttributes({
debugging: container.debugging,
insiders: insiders,
prerelease: prerelease,
});
container.telemetry.sendEvent('activate', data);
container.telemetry.sendEvent(
'activate',
{
'activation.elapsed': elapsed,
'activation.mode': mode?.name,
},
sw.startTime,
);
setTimeout(() => {
const data = flatten(configuration.getAll(true), { prefix: 'config', skipNulls: true, stringify: true });
const data = flatten(configuration.getAll(true), { prefix: 'config', stringify: 'all' });
// TODO@eamodio do we want to capture any vscode settings that are relevant to GitLens?
container.telemetry.sendEvent('config', data);
}, 5000);

+ 81
- 30
src/git/gitProviderService.ts View File

@ -25,7 +25,7 @@ import type { RepoComparisonKey } from '../repositories';
import { asRepoComparisonKey, Repositories } from '../repositories';
import type { Subscription } from '../subscription';
import { isSubscriptionPaidPlan, SubscriptionPlanId } from '../subscription';
import { groupByFilterMap, groupByMap } from '../system/array';
import { groupByFilterMap, groupByMap, joinUnique } from '../system/array';
import { gate } from '../system/decorators/gate';
import { debug, getLogScope, log } from '../system/decorators/log';
import { count, filter, first, flatMap, join, map, some } from '../system/iterable';
@ -112,16 +112,20 @@ export class GitProviderService implements Disposable {
return this._onDidChangeProviders.event;
}
private fireProvidersChanged(added?: GitProvider[], removed?: GitProvider[]) {
this.container.telemetry.setGlobalAttributes({
'providers.count': this._providers.size,
'providers.ids': join(this._providers.keys(), ','),
});
this._etag = Date.now();
this._onDidChangeProviders.fire({ added: added ?? [], removed: removed ?? [], etag: this._etag });
this.container.telemetry.sendEvent('providers/changed', {
'providers.count': this._providers.size,
'providers.ids': join(this._providers.keys(), ','),
'providers.added': added?.length ?? 0,
'providers.removed': removed?.length ?? 0,
'repositories.count': this.openRepositoryCount,
queueMicrotask(() => {
this.container.telemetry.sendEvent('providers/changed', {
'providers.added': added?.length ?? 0,
'providers.removed': removed?.length ?? 0,
});
});
}
@ -130,6 +134,12 @@ export class GitProviderService implements Disposable {
return this._onDidChangeRepositories.event;
}
private fireRepositoriesChanged(added?: Repository[], removed?: Repository[]) {
const openSchemes = this.openRepositories.map(r => r.uri.scheme);
this.container.telemetry.setGlobalAttributes({
'repositories.count': openSchemes.length,
'repositories.schemes': joinUnique(openSchemes, ','),
});
this._etag = Date.now();
this._accessCache.clear();
@ -139,12 +149,23 @@ export class GitProviderService implements Disposable {
}
this._onDidChangeRepositories.fire({ added: added ?? [], removed: removed ?? [], etag: this._etag });
this.container.telemetry.sendEvent('repositories/changed', {
'repositories.count': this.openRepositoryCount,
'repositories.added': added?.length ?? 0,
'repositories.removed': removed?.length ?? 0,
'providers.count': this._providers.size,
'providers.ids': join(this._providers.keys(), ','),
queueMicrotask(() => {
this.container.telemetry.sendEvent('repositories/changed', {
'repositories.added': added?.length ?? 0,
'repositories.removed': removed?.length ?? 0,
});
if (added?.length) {
for (const repo of added) {
this.container.telemetry.sendEvent('repository/opened', {
'repository.id': repo.idHash,
'repository.scheme': repo.uri.scheme,
'repository.closed': repo.closed,
'repository.folder.scheme': repo.folder?.uri.scheme,
'repository.provider.id': repo.provider.id,
});
}
}
});
}
@ -245,6 +266,12 @@ export class GitProviderService implements Disposable {
singleLine: true,
})
private onWorkspaceFoldersChanged(e: WorkspaceFoldersChangeEvent) {
const schemes = workspace.workspaceFolders?.map(f => f.uri.scheme);
this.container.telemetry.setGlobalAttributes({
'folders.count': schemes?.length ?? 0,
'folders.schemes': schemes != null ? joinUnique(schemes, ', ') : '',
});
if (e.added.length) {
void this.discoverRepositories(e.added);
}
@ -442,11 +469,12 @@ export class GitProviderService implements Disposable {
const autoRepositoryDetection = configuration.getAny<boolean | 'subFolders' | 'openEditors'>(
CoreGitConfiguration.AutoRepositoryDetection,
);
this.container.telemetry.sendEvent('providers/registrationComplete', {
'folders.count': workspaceFolders?.length ?? 0,
'folders.schemes': workspaceFolders?.map(f => f.uri.scheme).join(', '),
'config.git.autoRepositoryDetection': autoRepositoryDetection,
});
queueMicrotask(() =>
this.container.telemetry.sendEvent('providers/registrationComplete', {
'config.git.autoRepositoryDetection': autoRepositoryDetection,
}),
);
if (scope != null) {
scope.exitDetails = ` ${GlyphChars.Dot} workspaceFolders=${workspaceFolders?.length}, git.autoRepositoryDetection=${autoRepositoryDetection}`;
@ -672,16 +700,10 @@ export class GitProviderService implements Disposable {
let visibility = this._visibilityCache.get(undefined);
if (visibility == null) {
visibility = this.visibilityCore();
void visibility.then(v =>
queueMicrotask(() => {
this.container.telemetry.sendEvent('repositories/visibility', {
'repositories.count': this.openRepositoryCount,
'repositories.visibility': v,
'providers.count': this._providers.size,
'providers.ids': join(this._providers.keys(), ','),
});
}),
);
void visibility.then(v => {
this.container.telemetry.setGlobalAttribute('repositories.visibility', v);
this.container.telemetry.sendEvent('repositories/visibility');
});
this._visibilityCache.set(undefined, visibility);
}
return visibility;
@ -692,6 +714,19 @@ export class GitProviderService implements Disposable {
let visibility = this._visibilityCache.get(cacheKey);
if (visibility == null) {
visibility = this.visibilityCore(repoPath);
void visibility.then(v =>
queueMicrotask(() => {
const repo = this.getRepository(repoPath);
this.container.telemetry.sendEvent('repository/visibility', {
'repository.visibility': v,
'repository.id': repo?.idHash,
'repository.scheme': repo?.uri.scheme,
'repository.closed': repo?.closed,
'repository.folder.scheme': repo?.folder?.uri.scheme,
'repository.provider.id': repo?.provider.id,
});
}),
);
this._visibilityCache.set(cacheKey, visibility);
}
return visibility;
@ -783,11 +818,20 @@ export class GitProviderService implements Disposable {
}
private updateContext() {
const hasRepositories = this.openRepositoryCount !== 0;
const openRepositoryCount = this.openRepositoryCount;
const hasRepositories = openRepositoryCount !== 0;
void this.setEnabledContext(hasRepositories);
// Don't bother trying to set the values if we're still starting up
if (!hasRepositories && this._initializing) return;
if (this._initializing) return;
this.container.telemetry.setGlobalAttributes({
enabled: hasRepositories,
'repositories.count': openRepositoryCount,
});
if (!hasRepositories) return;
// Don't block for the remote context updates (because it can block other downstream requests during initialization)
async function updateRemoteContext(this: GitProviderService) {
@ -823,6 +867,13 @@ export class GitProviderService implements Disposable {
}
}
this.container.telemetry.setGlobalAttributes({
'repositories.hasRemotes': hasRemotes,
'repositories.hasRichRemotes': hasRichRemotes,
'repositories.hasConnectedRemotes': hasConnectedRemotes,
});
queueMicrotask(() => this.container.telemetry.sendEvent('providers/context'));
await Promise.allSettled([
setContext(ContextKeys.HasRemotes, hasRemotes),
setContext(ContextKeys.HasRichRemotes, hasRichRemotes),

+ 9
- 0
src/git/models/repository.ts View File

@ -18,6 +18,7 @@ import { debounce } from '../../system/function';
import { filter, join, some } from '../../system/iterable';
import { updateRecordValue } from '../../system/object';
import { basename, normalizePath } from '../../system/path';
import { md5 } from '../../system/string';
import { runGitCommandInTerminal } from '../../terminal';
import type { GitProviderDescriptor } from '../gitProvider';
import { loadRemoteProviders } from '../remotes/remoteProviders';
@ -180,6 +181,14 @@ export class Repository implements Disposable {
readonly index: number;
readonly name: string;
private _idHash: string | undefined;
get idHash() {
if (this._idHash === undefined) {
this._idHash = md5(this.id);
}
return this._idHash;
}
private _branch: Promise<GitBranch | undefined> | undefined;
private readonly _disposable: Disposable;
private _fireChangeDebounced: (() => void) | undefined = undefined;

+ 17
- 5
src/system/array.ts View File

@ -1,5 +1,6 @@
// eslint-disable-next-line no-restricted-imports
export { findLastIndex, intersectionWith as intersection } from 'lodash-es';
import { join } from './iterable';
export function chunk<T>(source: T[], size: number): T[][] {
const chunks = [];
@ -63,7 +64,7 @@ export function filterMapAsync(
}, []);
}
export function groupBy<T>(source: T[], groupingKey: (item: T) => string): Record<string, T[]> {
export function groupBy<T>(source: readonly T[], groupingKey: (item: T) => string): Record<string, T[]> {
return source.reduce<Record<string, T[]>>((groupings, current) => {
const value = groupingKey(current);
const group = groupings[value];
@ -76,7 +77,10 @@ export function groupBy(source: T[], groupingKey: (item: T) => string): Recor
}, Object.create(null));
}
export function groupByMap<TKey, TValue>(source: TValue[], groupingKey: (item: TValue) => TKey): Map<TKey, TValue[]> {
export function groupByMap<TKey, TValue>(
source: readonly TValue[],
groupingKey: (item: TValue) => TKey,
): Map<TKey, TValue[]> {
return source.reduce((groupings, current) => {
const value = groupingKey(current);
const group = groupings.get(value);
@ -90,7 +94,7 @@ export function groupByMap(source: TValue[], groupingKey: (item: T
}
export function groupByFilterMap<TKey, TValue, TMapped>(
source: TValue[],
source: readonly TValue[],
groupingKey: (item: TValue) => TKey,
predicateMapper: (item: TValue) => TMapped | null | undefined,
): Map<TKey, TMapped[]> {
@ -109,7 +113,7 @@ export function groupByFilterMap(
}, new Map<TKey, TMapped[]>());
}
export function isStringArray<T extends any[]>(array: string[] | T): array is string[] {
export function isStringArray<T extends any[]>(array: readonly string[] | T): array is string[] {
return typeof array[0] === 'string';
}
@ -206,8 +210,16 @@ export function compactHierarchy(
return root;
}
export function unique<T>(source: readonly T[]): T[] {
return [...new Set(source)];
}
export function joinUnique<T>(source: readonly T[], separator: string): string {
return join(new Set(source), separator);
}
export function uniqueBy<TKey, TValue>(
source: TValue[],
source: readonly TValue[],
uniqueKey: (item: TValue) => TKey,
onDuplicate: (original: TValue, current: TValue) => TValue | void,
): TValue[] {

+ 2
- 4
src/system/iterable.ts View File

@ -136,15 +136,13 @@ export function join(source: Iterable, separator: string): string {
if (next.done) return value;
while (true) {
const s = next.value.toString();
next = iterator.next();
if (next.done) {
value += s;
value += String(nextspan>.value);
break;
}
value += `${s}${separator}`;
value += `${nextspan>.value}${separator}`;
}
return value;

+ 6
- 2
src/system/object.ts View File

@ -12,11 +12,15 @@ export function flatten(
): Record<string, string | null>;
export function flatten(
o: any,
options: { prefix?: string; skipNulls?: false; stringify: 'all' },
): Record<string, string>;
export function flatten(
o: any,
options?: { prefix?: string; skipNulls?: boolean; stringify?: boolean },
): Record<string, any>;
export function flatten(
o: any,
options?: { prefix?: string; skipNulls?: boolean; stringify?: boolean },
options?: { prefix?: string; skipNulls?: boolean; stringify?: boolean | 'all' },
): Record<string, any> {
const skipNulls = options?.skipNulls ?? false;
const stringify = options?.stringify ?? false;
@ -26,7 +30,7 @@ export function flatten(
if (value == null) {
if (skipNulls) return;
flattened[key] = stringify ? value ?? null : value;
flattened[key] = stringify ? (stringify == 'all' ? JSON.stringify(value) : value ?? null) : value;
} else if (typeof value === 'string') {
flattened[key] = value;
} else {

+ 9
- 5
src/system/stopwatch.ts View File

@ -14,7 +14,11 @@ type StopwatchLogLevel = Exclude;
export class Stopwatch {
private readonly instance = `[${String(getNextLogScopeId()).padStart(5)}] `;
private readonly logLevel: StopwatchLogLevel;
private time: [number, number];
private _time: [number, number];
get startTime() {
return this._time;
}
constructor(public readonly scope: string | LogScope | undefined, options?: StopwatchOptions, ...params: any[]) {
let logScope;
@ -32,7 +36,7 @@ export class Stopwatch {
}
this.logLevel = options?.logLevel ?? LogLevel.Info;
this.time = hrtime();
this._time = hrtime();
if (logOptions != null) {
if (!Logger.enabled(this.logLevel)) return;
@ -55,7 +59,7 @@ export class Stopwatch {
}
elapsed(): number {
const [secs, nanosecs] = hrtime(this.time);
const [secs, nanosecs] = hrtime(this._time);
return secs * 1000 + Math.floor(nanosecs / 1000000);
}
@ -65,7 +69,7 @@ export class Stopwatch {
restart(options?: StopwatchLogOptions): void {
this.logCore(this.scope, options, true);
this.time = hrtime();
this._time = hrtime();
}
stop(options?: StopwatchLogOptions): void {
@ -91,7 +95,7 @@ export class Stopwatch {
return;
}
const [secs, nanosecs] = hrtime(this.time);
const [secs, nanosecs] = hrtime(this._time);
const ms = secs * 1000 + Math.floor(nanosecs / 1000000);
const prefix = `${this.instance}${scope}${options?.message ?? ''}`;

+ 6
- 6
src/telemetry/openTelemetryProvider.ts View File

@ -1,4 +1,4 @@
import type { AttributeValue, Span, Tracer } from '@opentelemetry/api';
import type { AttributeValue, Span, TimeInput, Tracer } from '@opentelemetry/api';
import { diag, DiagConsoleLogger, trace } from '@opentelemetry/api';
import { DiagLogLevel } from '@opentelemetry/api/build/src/diag/types';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
@ -55,17 +55,17 @@ export class OpenTelemetryProvider implements TelemetryProvider {
trace.disable();
}
sendEvent(name: string, data?: Record<string, AttributeValue>): void {
const span = this.tracer.startSpan(name, { startTime: Date.now() });
sendEvent(name: string, data?: Record<string, AttributeValue>, startTime?: TimeInput, endTime?: TimeInput): void {
const span = this.tracer.startSpan(name, { startTime: startTime ?? Date.now() });
span.setAttributes(this._globalAttributes);
if (data != null) {
span.setAttributes(data);
}
span.end();
span.end(endTime);
}
startEvent(name: string, data?: Record<string, AttributeValue>): Span {
const span = this.tracer.startSpan(name, { startTime: Date.now() });
startEvent(name: string, data?: Record<string, AttributeValue>, startTime?: TimeInput): Span {
const span = this.tracer.startSpan(name, { startTime: startTime ?? Date.now() });
span.setAttributes(this._globalAttributes);
if (data != null) {
span.setAttributes(data);

+ 42
- 24
src/telemetry/telemetry.ts View File

@ -1,4 +1,4 @@
import type { AttributeValue, Span } from '@opentelemetry/api';
import type { AttributeValue, Span, TimeInput } from '@opentelemetry/api';
import type { Disposable } from 'vscode';
import { version as codeVersion, env } from 'vscode';
import { getProxyAgent } from '@env/fetch';
@ -20,15 +20,17 @@ export interface TelemetryContext {
}
export interface TelemetryProvider extends Disposable {
sendEvent(name: string, data?: Record<string, AttributeValue>): void;
startEvent(name: string, data?: Record<string, AttributeValue>): Span;
sendEvent(name: string, data?: Record<string, AttributeValue>, startTime?: TimeInput, endTime?: TimeInput): void;
startEvent(name: string, data?: Record<string, AttributeValue>, startTime?: TimeInput): Span;
setGlobalAttributes(attributes: Map<string, AttributeValue>): void;
}
interface QueuedEvent {
type: 'sendEvent';
name: string;
data?: Record<string, AttributeValue>;
data?: Record<string, AttributeValue | null | undefined>;
startTime: TimeInput;
endTime: TimeInput;
}
export class TelemetryService implements Disposable {
@ -108,35 +110,52 @@ export class TelemetryService implements Disposable {
for (const { type, name, data } of queue) {
if (type === 'sendEvent') {
this.provider.sendEvent(name, data);
this.provider.sendEvent(name, stripNullOrUndefinedAttributes(data));
}
}
}
}
sendEvent(name: string, data?: Record<string, AttributeValue | null | undefined>): void {
sendEvent(
name: string,
data?: Record<string, AttributeValue | null | undefined>,
startTime?: TimeInput,
endTime?: TimeInput,
): void {
if (!this.enabled) return;
const attributes = stripNullOrUndefinedAttributes(data);
if (this.provider == null) {
this.eventQueue.push({ type: 'sendEvent', name: name, data: attributes });
this.eventQueue.push({
type: 'sendEvent',
name: name,
data: data,
startTime: startTime ?? Date.now(),
endTime: endTime ?? Date.now(),
});
return;
}
this.provider.sendEvent(name, attributes);
this.provider.sendEvent(name, stripNullOrUndefinedAttributes(data), startTime, endTime);
}
async startEvent(
startEvent(
name: string,
data?: Record<string, AttributeValue | null | undefined>,
): Promise<Span | undefined> {
if (!this.enabled) return;
if (this.provider == null) {
await this.initializeTelemetry(this.container);
startTime?: TimeInput,
): Disposable | undefined {
if (!this.enabled) return undefined;
if (this.provider != null) {
const span = this.provider.startEvent(name, stripNullOrUndefinedAttributes(data), startTime);
return {
dispose: () => span?.end(),
};
}
const attributes = stripNullOrUndefinedAttributes(data);
return this.provider!.startEvent(name, attributes);
startTime = startTime ?? Date.now();
return {
dispose: () => this.sendEvent(name, data, startTime, Date.now()),
};
}
// sendErrorEvent(
@ -178,14 +197,13 @@ export class TelemetryService implements Disposable {
}
function stripNullOrUndefinedAttributes(data: Record<string, AttributeValue | null | undefined> | undefined) {
let attributes: Record<string, AttributeValue> | undefined;
if (data != null) {
attributes = Object.create(null);
for (const [key, value] of Object.entries(data)) {
if (value == null) continue;
if (data == null) return undefined;
attributes![key] = value;
}
const attributes: Record<string, AttributeValue> | undefined = Object.create(null);
for (const [key, value] of Object.entries(data)) {
if (value == null) continue;
attributes![key] = value;
}
return attributes;
}

Loading…
Cancel
Save