diff --git a/src/analytics/OtelPosthogExporter.ts b/src/analytics/OtelPosthogExporter.ts
index 8f9ba26..c624a00 100644
--- a/src/analytics/OtelPosthogExporter.ts
+++ b/src/analytics/OtelPosthogExporter.ts
@@ -16,12 +16,29 @@ limitations under the License.
 
 import { SpanExporter, ReadableSpan } from "@opentelemetry/sdk-trace-base";
 import { ExportResult, ExportResultCode } from "@opentelemetry/core";
+import { logger } from "matrix-js-sdk/src/logger";
+import { HrTime } from "@opentelemetry/api";
 
 import { PosthogAnalytics } from "./PosthogAnalytics";
 
+interface PrevCall {
+  callId: string;
+  hangupTs: number;
+}
+
+function hrTimeToMs(time: HrTime): number {
+  return time[0] * 1000 + time[1] * 0.000001;
+}
+
 /**
- * This is implementation of {@link SpanExporter} that sends spans
- * to Posthog
+ * The maximum time between hanging up and joining the same call that we would
+ * consider a 'rejoin' on the user's part.
+ */
+const maxRejoinMs = 2 * 60 * 1000; // 2 minutes
+
+/**
+ * This is implementation of {@link SpanExporter} that extracts certain metrics
+ * from spans to send to PostHog
  */
 export class PosthogSpanExporter implements SpanExporter {
   /**
@@ -33,41 +50,68 @@ export class PosthogSpanExporter implements SpanExporter {
     spans: ReadableSpan[],
     resultCallback: (result: ExportResult) => void
   ): Promise<void> {
-    console.log("POSTHOGEXPORTER", spans);
-    for (const span of spans) {
-      const sendInstantly = [
-        "otel_callEnded",
-        "otel_otherSentInstantlyEventName",
-      ].includes(span.name);
-
-      for (const spanEvent of span.events) {
-        await PosthogAnalytics.instance.trackFromSpan(
-          {
-            eventName: spanEvent.name,
-            ...spanEvent.attributes,
-          },
-          {
-            send_instantly: sendInstantly,
-          }
-        );
-      }
-
-      await PosthogAnalytics.instance.trackFromSpan(
-        { eventName: span.name, ...span.attributes },
-        {
-          send_instantly: sendInstantly,
+    await Promise.all(
+      spans.map((span) => {
+        switch (span.name) {
+          case "matrix.groupCallMembership":
+            return this.exportGroupCallMembershipSpan(span);
+          // TBD if there are other spans that we want to process for export to
+          // PostHog
         }
-      );
-      resultCallback({ code: ExportResultCode.SUCCESS });
+      })
+    );
+
+    resultCallback({ code: ExportResultCode.SUCCESS });
+  }
+
+  private get prevCall(): PrevCall | null {
+    // This is stored in localStorage so we can remember the previous call
+    // across app restarts
+    const data = localStorage.getItem("matrix-prev-call");
+    if (data === null) return null;
+
+    try {
+      return JSON.parse(data);
+    } catch (e) {
+      logger.warn("Invalid prev call data", data);
+      return null;
     }
   }
+
+  private set prevCall(data: PrevCall | null) {
+    localStorage.setItem("matrix-prev-call", JSON.stringify(data));
+  }
+
+  async exportGroupCallMembershipSpan(span: ReadableSpan): Promise<void> {
+    const prevCall = this.prevCall;
+    const newPrevCall = (this.prevCall = {
+      callId: span.attributes["matrix.confId"] as string,
+      hangupTs: hrTimeToMs(span.endTime),
+    });
+
+    // If the user joined the same call within a short time frame, log this as a
+    // rejoin. This is interesting as a call quality metric, since rejoins may
+    // indicate that users had to intervene to make the product work.
+    if (prevCall !== null && newPrevCall.callId === prevCall.callId) {
+      const duration = hrTimeToMs(span.startTime) - prevCall.hangupTs;
+      if (duration <= maxRejoinMs) {
+        PosthogAnalytics.instance.trackEvent(
+          {
+            eventName: "Rejoin",
+            callId: prevCall.callId,
+            rejoinDuration: duration,
+          },
+          // Send instantly because the window might be closing
+          { send_instantly: true }
+        );
+      }
+    }
+  }
+
   /**
    * Shutdown the exporter.
    */
   shutdown(): Promise<void> {
-    console.log("POSTHOGEXPORTER shutdown of otelPosthogExporter");
-    return new Promise<void>((resolve, _reject) => {
-      resolve();
-    });
+    return Promise.resolve();
   }
 }
diff --git a/src/analytics/PosthogAnalytics.ts b/src/analytics/PosthogAnalytics.ts
index 718a49c..e2e8fda 100644
--- a/src/analytics/PosthogAnalytics.ts
+++ b/src/analytics/PosthogAnalytics.ts
@@ -385,22 +385,6 @@ 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 -