Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Update analytics for SlashCommands #11264

Closed
wants to merge 20 commits into from
Closed
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
64 changes: 64 additions & 0 deletions src/components/views/rooms/BasicMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ limitations under the License.

import classNames from "classnames";
import React, { createRef, ClipboardEvent, SyntheticEvent } from "react";
import { FormattedMessage as FormattedMessageEvent } from "@matrix-org/analytics-events/types/typescript/FormattedMessage";
import { Mention as MentionEvent } from "@matrix-org/analytics-events/types/typescript/Mention";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import EMOTICON_REGEX from "emojibase-regex/emoticon";
Expand Down Expand Up @@ -52,6 +54,7 @@ import { _t } from "../../../languageHandler";
import { linkify } from "../../../linkify-matrix";
import { SdkContextClass } from "../../../contexts/SDKContext";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { PosthogAnalytics } from "../../../PosthogAnalytics";

// matches emoticons which follow the start of a line or whitespace
const REGEX_EMOTICON_WHITESPACE = new RegExp("(?:^|\\s)(" + EMOTICON_REGEX.source + ")\\s|:^$");
Expand Down Expand Up @@ -682,6 +685,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>

private onAutoCompleteConfirm = (completion: ICompletion): void => {
this.modifiedFlag = true;

// send analytics events for mentions when we select them
if (completion.type === "room" || completion.type === "at-room") {
trackMentionAnalyticEvent("Room");
} else if (completion.type === "user") {
trackMentionAnalyticEvent("User");
}

this.props.model.autoComplete?.onComponentConfirm(completion);
};

Expand Down Expand Up @@ -771,6 +782,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
return;
}

trackFormattingAnalyticEvent(action);

const range: Range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection()!);

this.historyManager.ensureLastChangesPushed(this.props.model);
Expand Down Expand Up @@ -906,3 +919,54 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
});
}
}

/**
* Util function to convert a `Formatting` action to a formatting analytic event and then fire that event
*
* @param action - the formatting action that will be recorded in the analytic event that is fired
* @returns void
*/
function trackFormattingAnalyticEvent(action: Formatting): void {
let formatAction: FormattedMessageEvent["formatAction"];

switch (action) {
case Formatting.Bold:
formatAction = "Bold";
break;
case Formatting.Italics:
formatAction = "Italic";
break;
case Formatting.Strikethrough:
formatAction = "Strikethrough";
break;
case Formatting.Code:
formatAction = "InlineCode";
break;
case Formatting.Quote:
formatAction = "Quote";
break;
case Formatting.InsertLink:
formatAction = "Link";
break;
}

PosthogAnalytics.instance.trackEvent<FormattedMessageEvent>({
eventName: "FormattedMessage",
editor: "RteFormatting",
formatAction,
});
}

/**
* Util function to fire a mention analytic event
*
* @param targetType - the editor type that will be recorded in the analytic event that is fired
* @returns void
*/
function trackMentionAnalyticEvent(targetType: MentionEvent["targetType"]): void {
PosthogAnalytics.instance.trackEvent<MentionEvent>({
eventName: "Mention",
editor: "RteFormatting",
targetType,
});
}
3 changes: 3 additions & 0 deletions src/components/views/rooms/EditMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,9 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
isEditing: true,
inThread: !!editedEvent?.getThread(),
isReply: !!editedEvent.replyEventId,
isLocation: false,
editor: "Legacy",
isMarkdownEnabled: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
});

// Replace emoticon at the end of the message
Expand Down
7 changes: 7 additions & 0 deletions src/components/views/rooms/SendMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { doMaybeLocalRoomAction } from "../../../utils/local-room";
import { Caret } from "../../../editor/caret";
import { IDiff } from "../../../editor/diff";
import { getBlobSafeMimeType } from "../../../utils/blobs";
import { trackSlashCommandAnalyticEvent } from "./wysiwyg_composer/utils/message";

/**
* Build the mentions information based on the editor model (and any related events):
Expand Down Expand Up @@ -181,6 +182,7 @@ export function createMessageContent(
const isEmote = containsEmote(model);
if (isEmote) {
model = stripEmoteCommand(model);
trackSlashCommandAnalyticEvent("me", "Legacy");
}
if (startsWith(model, "//")) {
model = stripPrefix(model, "/");
Expand Down Expand Up @@ -449,6 +451,9 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
isEditing: false,
isReply: !!this.props.replyToEvent,
inThread: this.props.relation?.rel_type === THREAD_RELATION_TYPE.name,
isLocation: false,
editor: "Legacy",
isMarkdownEnabled: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
};
if (posthogEvent.inThread && this.props.relation!.event_id) {
const threadRoot = this.props.room.findEventById(this.props.relation!.event_id);
Expand Down Expand Up @@ -488,6 +493,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
return; // errored
}

trackSlashCommandAnalyticEvent(cmd.command, "Legacy");

if (content && [CommandCategories.messages, CommandCategories.effects].includes(cmd.category)) {
// Attach any mentions which might be contained in the command content.
attachMentions(this.props.mxClient.getSafeUserId(), content, model, replyToEvent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.

import React, { MouseEventHandler, ReactNode } from "react";
import { FormattingFunctions, AllActionStates, ActionState } from "@matrix-org/matrix-wysiwyg";
import { FormattedMessage as FormattedMessageEvent } from "@matrix-org/analytics-events/types/typescript/FormattedMessage";
import classNames from "classnames";

import { Icon as BoldIcon } from "../../../../../../res/img/element-icons/room/composer/bold.svg";
Expand All @@ -38,6 +39,8 @@ import { _td } from "../../../../../languageHandler";
import { ButtonEvent } from "../../../elements/AccessibleButton";
import { openLinkModal } from "./LinkModal";
import { useComposerContext } from "../ComposerContext";
import { isNotUndefined } from "../../../../../Typeguards";
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";

interface TooltipProps {
label: string;
Expand All @@ -59,9 +62,15 @@ interface ButtonProps extends TooltipProps {
icon: ReactNode;
actionState: ActionState;
onClick: MouseEventHandler<HTMLButtonElement>;
analyticsKey?: FormattedMessageEvent["formatAction"];
}

function Button({ label, keyCombo, onClick, actionState, icon }: ButtonProps): JSX.Element {
function Button({ analyticsKey, label, keyCombo, onClick, actionState, icon }: ButtonProps): JSX.Element {
const prevActionState = React.useRef(actionState);
if (isNotUndefined(analyticsKey) && prevActionState.current !== actionState && actionState === "reversed") {
trackFormattingAnalyticEvent(analyticsKey);
}
prevActionState.current = actionState;
return (
<AccessibleTooltipButton
element="button"
Expand Down Expand Up @@ -92,85 +101,123 @@ export function FormattingButtons({ composer, actionStates }: FormattingButtonsP
return (
<div className="mx_FormattingButtons">
<Button
analyticsKey="Bold"
actionState={actionStates.bold}
label={_td("Bold")}
keyCombo={{ ctrlOrCmdKey: true, key: "b" }}
onClick={() => composer.bold()}
icon={<BoldIcon className="mx_FormattingButtons_Icon" />}
/>
<Button
analyticsKey="Italic"
actionState={actionStates.italic}
label={_td("Italic")}
keyCombo={{ ctrlOrCmdKey: true, key: "i" }}
onClick={() => composer.italic()}
icon={<ItalicIcon className="mx_FormattingButtons_Icon" />}
/>
<Button
analyticsKey="Underline"
actionState={actionStates.underline}
label={_td("Underline")}
keyCombo={{ ctrlOrCmdKey: true, key: "u" }}
onClick={() => composer.underline()}
icon={<UnderlineIcon className="mx_FormattingButtons_Icon" />}
/>
<Button
analyticsKey="Strikethrough"
actionState={actionStates.strikeThrough}
label={_td("Strikethrough")}
onClick={() => composer.strikeThrough()}
icon={<StrikeThroughIcon className="mx_FormattingButtons_Icon" />}
/>
<Button
analyticsKey="UnorderedList"
actionState={actionStates.unorderedList}
label={_td("Bulleted list")}
onClick={() => composer.unorderedList()}
icon={<BulletedListIcon className="mx_FormattingButtons_Icon" />}
/>
<Button
analyticsKey="OrderedList"
actionState={actionStates.orderedList}
label={_td("Numbered list")}
onClick={() => composer.orderedList()}
icon={<NumberedListIcon className="mx_FormattingButtons_Icon" />}
/>
{/* Neither of the indent or unindent buttons can be triggered by a keyboard shorcut. Their states also
only toggle between `disabled` and `enabled`, which presents ambiguity as to whether they have been clicked
(state goes from `enabled` => `disabled`) or they were available to click and then the list was toggled off
(as this causes the same state transition). Use the user click to record interaction*/}
{isInList && (
<Button
actionState={actionStates.indent}
label={_td("Indent increase")}
onClick={() => composer.indent()}
onClick={() => {
composer.indent();
trackFormattingAnalyticEvent("Indent");
}}
icon={<IndentIcon className="mx_FormattingButtons_Icon" />}
/>
)}
{isInList && (
<Button
actionState={actionStates.unindent}
label={_td("Indent decrease")}
onClick={() => composer.unindent()}
onClick={() => {
composer.unindent();
trackFormattingAnalyticEvent("Unindent");
}}
icon={<UnIndentIcon className="mx_FormattingButtons_Icon" />}
/>
)}
<Button
analyticsKey="Quote"
actionState={actionStates.quote}
label={_td("Quote")}
onClick={() => composer.quote()}
icon={<QuoteIcon className="mx_FormattingButtons_Icon" />}
/>
<Button
analyticsKey="InlineCode"
actionState={actionStates.inlineCode}
label={_td("Code")}
keyCombo={{ ctrlOrCmdKey: true, key: "e" }}
onClick={() => composer.inlineCode()}
icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />}
/>
<Button
analyticsKey="CodeBlock"
actionState={actionStates.codeBlock}
label={_td("Code block")}
onClick={() => composer.codeBlock()}
icon={<CodeBlockIcon className="mx_FormattingButtons_Icon" />}
/>
{/* Inserting a link works differently to the rest of the buttons and has no keyboard shortcut, so
fire an analytic event onClick */}
<Button
analyticsKey="Link"
actionState={actionStates.link}
label={_td("Link")}
onClick={() => openLinkModal(composer, composerContext, actionStates.link === "reversed")}
onClick={() => {
openLinkModal(composer, composerContext, actionStates.link === "reversed");
trackFormattingAnalyticEvent("Link");
}}
icon={<LinkIcon className="mx_FormattingButtons_Icon" />}
/>
</div>
);
}

/**
* Util function to fire a formatting analytic event
* @param formatAction - the action that will be recorded in the analytic event that is fired
* @returns void
*/
function trackFormattingAnalyticEvent(formatAction: FormattedMessageEvent["formatAction"]): void {
PosthogAnalytics.instance.trackEvent<FormattedMessageEvent>({
eventName: "FormattedMessage",
editor: "RteFormatting",
formatAction,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export function PlainTextComposer({
onSelect={onSelect}
>
<WysiwygAutocomplete
analyticsEditor="RtePlain"
ref={autocompleteRef}
suggestion={suggestion}
handleMention={handleMention}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,21 @@ limitations under the License.

import React, { ForwardedRef, forwardRef } from "react";
import { FormattingFunctions, MappedSuggestion } from "@matrix-org/matrix-wysiwyg";
import { Mention as MentionEvent } from "@matrix-org/analytics-events/types/typescript/Mention";

import { useRoomContext } from "../../../../../contexts/RoomContext";
import Autocomplete from "../../Autocomplete";
import { ICompletion } from "../../../../../autocomplete/Autocompleter";
import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext";
import { getMentionDisplayText, getMentionAttributes, buildQuery } from "../utils/autocomplete";
import { PosthogAnalytics } from "../../../../../PosthogAnalytics";

interface WysiwygAutocompleteProps {
/**
* The editor mode that is using this component, used to generate analytics around use of Mentions.
*/
analyticsEditor: Exclude<MentionEvent["editor"], "Legacy">;

/**
* The suggestion output from the rust model is used to build the query that is
* passed to the `<Autocomplete />` component
Expand Down Expand Up @@ -68,7 +75,16 @@ const WysiwygAutocomplete = forwardRef(
return;
}

switch (completion.type) {
const { type, href } = completion;

// fire analytics tracking events if required
if (type === "room" || type === "at-room") {
trackMentionAnalyticEvent("Room");
} else if (type === "user") {
trackMentionAnalyticEvent("User");
}

switch (type) {
case "command": {
// TODO determine if utils in SlashCommands.tsx are required.
// Trim the completion as some include trailing spaces, but we always insert a
Expand All @@ -82,9 +98,9 @@ const WysiwygAutocomplete = forwardRef(
}
case "room":
case "user": {
if (typeof completion.href === "string") {
if (typeof href === "string") {
handleMention(
completion.href,
href,
getMentionDisplayText(completion, client),
getMentionAttributes(completion, client, room),
);
Expand Down Expand Up @@ -116,3 +132,17 @@ const WysiwygAutocomplete = forwardRef(
WysiwygAutocomplete.displayName = "WysiwygAutocomplete";

export { WysiwygAutocomplete };

/**
* Util function to fire a mention analytic event
*
* @param targetType - the editor type that will be recorded in the analytic event that is fired
* @returns void
*/
function trackMentionAnalyticEvent(targetType: MentionEvent["targetType"]): void {
PosthogAnalytics.instance.trackEvent<MentionEvent>({
eventName: "Mention",
editor: "RteFormatting",
targetType,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({
onBlur={onFocus}
>
<WysiwygAutocomplete
analyticsEditor="RteFormatting"
ref={autocompleteRef}
suggestion={suggestion}
handleMention={wysiwyg.mention}
Expand Down
Loading