otel for call start, end and mute

This is send over zipkin.
And it uses a posthog exporter to export events to posthog
using a _otel prefix
This commit is contained in:
Timo K 2023-03-10 10:33:54 +01:00
commit 4c59638d00
7 changed files with 490 additions and 12 deletions

View file

@ -0,0 +1,57 @@
import { SpanExporter } from "@opentelemetry/sdk-trace-base";
import { ReadableSpan } from "@opentelemetry/sdk-trace-base";
import { ExportResult, ExportResultCode } from "@opentelemetry/core";
import { PosthogAnalytics } from "./PosthogAnalytics";
/**
* This is implementation of {@link SpanExporter} that prints spans to the
* console. This class can be used for diagnostic purposes.
*/
export class PosthogSpanExporter implements SpanExporter {
/**
* Export spans.
* @param spans
* @param resultCallback
*/
async export(
spans: ReadableSpan[],
resultCallback: (result: ExportResult) => void
): Promise<void> {
console.log("POSTHOGEXPORTER", spans);
for (let i = 0; i < spans.length; i++) {
const span = spans[i];
const sendInstantly =
span.name == "otel_callEnded" ||
span.name == "otel_otherSentInstantlyEventName";
await PosthogAnalytics.instance.trackFromSpan(
{ eventName: span.name, ...span.attributes },
{
send_instantly: sendInstantly,
}
);
resultCallback({ code: ExportResultCode.SUCCESS });
}
}
/**
* Shutdown the exporter.
*/
shutdown(): Promise<void> {
console.log("POSTHOGEXPORTER shutdown of otelPosthogExporter");
return new Promise<void>((resolve, _reject) => {
resolve();
});
}
/**
* converts span info into more readable format
* @param span
*/
// private _exportInfo;
/**
* Showing spans in console
* @param spans
* @param done
*/
// private _sendSpans;
}
//# sourceMappingURL=ConsoleSpanExporter.d.ts.map

View file

@ -385,6 +385,22 @@ export class PosthogAnalytics {
this.capture(eventName, properties, options);
}
public async trackFromSpan(
{ eventName, ...properties },
options?: CaptureOptions
): Promise<void> {
if (this.identificationPromise) {
// only make calls to posthog after the identificaion is done
await this.identificationPromise;
}
if (
this.anonymity == Anonymity.Disabled ||
this.anonymity == Anonymity.Anonymous
)
return;
this.capture(eventName, properties, options);
}
public startListeningToSettingsChanges(): void {
// Listen to account data changes from sync so we can observe changes to relevant flags and update.
// This is called -

View file

@ -35,6 +35,7 @@ import { useLocationNavigation } from "../useLocationNavigation";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useMediaHandler } from "../settings/useMediaHandler";
import { findDeviceByName, getDevices } from "../media-utils";
import { callTracer } from "../telemetry/otel";
declare global {
interface Window {
@ -143,7 +144,7 @@ export function GroupCallView({
]);
await groupCall.enter();
callTracer.startCall(groupCall.groupCallId);
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
@ -164,6 +165,7 @@ export function GroupCallView({
if (isEmbedded && !preload) {
// In embedded mode, bypass the lobby and just enter the call straight away
groupCall.enter();
callTracer.startCall(groupCall.groupCallId);
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
@ -187,6 +189,7 @@ export function GroupCallView({
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
// therefore we want the event to be sent instantly without getting queued/batched.
callTracer.endCall();
const sendInstantly = !!widget;
PosthogAnalytics.instance.eventCallEnded.track(
groupCall.groupCallId,

View file

@ -32,6 +32,7 @@ import { usePageUnload } from "./usePageUnload";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { TranslatedError, translatedError } from "../TranslatedError";
import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
import { callTracer } from "../telemetry/otel";
export enum ConnectionState {
EstablishingCall = "establishing call", // call hasn't been established yet
@ -375,6 +376,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
) {
return;
}
callTracer.startCall(groupCall.groupCallId);
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
@ -399,6 +401,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
const setMicrophoneMuted = useCallback(
(setMuted) => {
groupCall.setMicrophoneMuted(setMuted);
callTracer.muteMic(setMuted);
PosthogAnalytics.instance.eventMuteMicrophone.track(
setMuted,
groupCall.groupCallId

128
src/telemetry/otel.ts Normal file
View file

@ -0,0 +1,128 @@
/* document-load.ts|js file - the code is the same for both the languages */
import {
ConsoleSpanExporter,
SimpleSpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import { ZipkinExporter } from "@opentelemetry/exporter-zipkin";
// import { JaegerExporter } from "@opentelemetry/exporter-jaeger";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
import { ZoneContextManager } from "@opentelemetry/context-zone";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
import opentelemetry from "@opentelemetry/api";
import { Resource } from "@opentelemetry/resources";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
import { PosthogSpanExporter } from "../analytics/OtelPosthogExporter";
const SERVICE_NAME = "element-call";
// It is really important to set the correct content type here. Otherwise the Jaeger will crash and not accept the zipkin event
// Additionally jaeger needs to be started with zipkin on port 9411
const optionsZipkin = {
// url: `http://localhost:9411/api/v2/spans`,
// serviceName: SERVICE_NAME,
headers: {
"Content-Type": "application/json",
},
};
// We DO NOT use the OTLPTraceExporter. This somehow does not hit the right endpoint and also causes issues with CORS
const collectorOptions = {
// url: `http://localhost:14268/api/v2/spans`, // url is optional and can be omitted - default is http://localhost:4318/v1/traces
headers: { "Access-Control-Allow-Origin": "*" }, // an optional object containing custom headers to be sent with each request
concurrencyLimit: 10, // an optional limit on pending requests
};
const otlpExporter = new OTLPTraceExporter(collectorOptions);
const consoleExporter = new ConsoleSpanExporter();
// The zipkin exporter is the actual exporter we need for web based otel applications
const zipkin = new ZipkinExporter(optionsZipkin);
const posthogExporter = new PosthogSpanExporter();
// This is how we can make Jaeger show a reaonsable service in the dropdown on the left.
const providerConfig = {
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME,
}),
};
const provider = new WebTracerProvider(providerConfig);
provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter));
// We can add as many processors and exporters as we want to. The zipkin one is the important one for Jaeger
provider.addSpanProcessor(new SimpleSpanProcessor(posthogExporter));
provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter));
provider.addSpanProcessor(new SimpleSpanProcessor(zipkin));
// This is unecassary i think...
provider.register({
// Changing default contextManager to use ZoneContextManager - supports asynchronous operations - optional
contextManager: new ZoneContextManager(),
});
// Registering instrumentations (These are automated span collectors for the Http request during page loading, switching)
registerInstrumentations({
instrumentations: [
// new DocumentLoadInstrumentation(),
// new UserInteractionInstrumentation(),
],
});
// This is not the serviceName shown in jaeger
export const tracer = opentelemetry.trace.getTracer(
"my-element-call-otl-tracer"
);
class CallTracer {
// We create one tracer class for each main context.
// Even if differnt tracer classes overlap in time space, we might want to visulaize them seperately.
// The Call Tracer should only contain spans/events that are relevant to understand the procedure of the individual candidates.
// Another Tracer Class (for example a ConnectionTracer) can contain a very granular list of all steps to connect to a call.
private callSpan;
private callContext;
private muteSpan?;
public startCall(callId: string) {
// The main context will be set when initiating the main/parent span.
// Create an initial context with the callId param
const callIdContext = opentelemetry.context
.active()
.setValue(Symbol("callId"), callId);
// Create the main span that tracks the whole call
this.callSpan = tracer.startSpan("otel_callSpan", undefined, callIdContext);
// Create a new call based on the callIdContext. This context also has a span assigned to it.
// Other spans can use this context to extract the parent span.
// (When passing this context to startSpan the started span will use the span set in the context (in this case the callSpan) as the parent)
this.callContext = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
this.callSpan
);
// Here we start a very short span. This is a hack to trigger the posthog exporter.
// Only ended spans are processed by the exporter.
// We want the exporter to know that a call has started
const startCallSpan = tracer.startSpan(
"otel_startCallSpan",
undefined,
this.callContext
);
startCallSpan.end();
}
public muteMic(muteState: boolean) {
if (muteState) {
this.muteSpan = tracer.startSpan(
"otel_muteSpan",
undefined,
this.callContext
);
} else if (this.muteSpan) {
this.muteSpan.end();
this.muteSpan = null;
}
}
public endCall() {
this.callSpan?.end();
}
}
export const callTracer = new CallTracer();