@planningcenter/chat-react-native 3.12.2-rc.0 → 3.12.2-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/components/conversation/attachments/image_attachment.d.ts.map +1 -1
- package/build/components/conversation/attachments/image_attachment.js +12 -6
- package/build/components/conversation/attachments/image_attachment.js.map +1 -1
- package/build/hooks/use_conversation_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversation_jolt_events.js +14 -2
- package/build/hooks/use_conversation_jolt_events.js.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.js +6 -0
- package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
- package/build/hooks/use_conversations_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversations_jolt_events.js +13 -1
- package/build/hooks/use_conversations_jolt_events.js.map +1 -1
- package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
- package/build/hooks/use_message_create_or_update.js +22 -1
- package/build/hooks/use_message_create_or_update.js.map +1 -1
- package/build/types/jolt_events/conversation_events.d.ts +1 -0
- package/build/types/jolt_events/conversation_events.d.ts.map +1 -1
- package/build/types/jolt_events/conversation_events.js.map +1 -1
- package/build/types/jolt_events/message_events.d.ts +1 -0
- package/build/types/jolt_events/message_events.d.ts.map +1 -1
- package/build/types/jolt_events/message_events.js.map +1 -1
- package/build/utils/performance_tracking.d.ts +26 -0
- package/build/utils/performance_tracking.d.ts.map +1 -0
- package/build/utils/performance_tracking.js +100 -0
- package/build/utils/performance_tracking.js.map +1 -0
- package/package.json +2 -2
- package/src/components/conversation/attachments/image_attachment.tsx +24 -19
- package/src/hooks/use_conversation_jolt_events.ts +19 -3
- package/src/hooks/use_conversation_messages_jolt_events.ts +6 -0
- package/src/hooks/use_conversations_jolt_events.ts +14 -2
- package/src/hooks/use_message_create_or_update.ts +23 -1
- package/src/types/jolt_events/conversation_events.ts +1 -0
- package/src/types/jolt_events/message_events.ts +1 -0
- package/src/utils/performance_tracking.ts +128 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use_message_create_or_update.js","sourceRoot":"","sources":["../../src/hooks/use_message_create_or_update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACjE,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AACzF,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE/C,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC1D,OAAO,EACL,uBAAuB,EACvB,+BAA+B,EAC/B,uBAAuB,GACxB,MAAM,UAAU,CAAA;AAEjB,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,2BAA2B,EAAE,MAAM,8CAA8C,CAAA;AAC1F,OAAO,EAAE,2BAA2B,EAAE,MAAM,8CAA8C,CAAA;
|
|
1
|
+
{"version":3,"file":"use_message_create_or_update.js","sourceRoot":"","sources":["../../src/hooks/use_message_create_or_update.ts"],"names":[],"mappings":"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","sourcesContent":["import { InfiniteData, useMutation } from '@tanstack/react-query'\nimport { getMessagesQueryKey, getMessagesRequestArgs } from './use_conversation_messages'\nimport { useApiClient } from './use_api_client'\nimport { ApiCollection, ApiResource, MessageResource } from '../types'\nimport { chatQueryClient } from '../contexts/api_provider'\nimport {\n deleteRecordInPagesData,\n updateOrCreateRecordInPagesData,\n updateRecordInPagesData,\n} from '../utils'\nimport { DenormalizedAttachmentResourceForCreate } from '../types/resources/denormalized_attachment_resource_for_create'\nimport { useCurrentPerson } from './use_current_person'\nimport { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message'\nimport { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message'\nimport { startMessageCreationTracking } from '../utils/performance_tracking'\n\ninterface Props {\n conversationId: number\n message?: MessageResource\n}\n\nexport function useMessageCreateOrUpdate({ conversationId, message }: Props) {\n const messageId = message?.id || null\n const isEditing = !isNewMessage(message)\n const apiClient = useApiClient()\n const currentPerson = useCurrentPerson()\n\n const mutation = useMutation({\n mutationFn: ({\n text,\n attachments,\n }: {\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n }) => {\n const requestParams = getMessagesRequestArgs({ conversation_id: conversationId })\n const fieldsWithValueJoined = Object.fromEntries(\n Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])\n )\n let attributes: any = { text, ...(attachments ? { attachments } : {}) }\n if (!isEditing) {\n const idempotentKey = insecureUUID()\n attributes.idempotent_key = idempotentKey\n startMessageCreationTracking(idempotentKey)\n }\n const data = {\n ...requestParams.data,\n data: {\n type: 'Message',\n attributes,\n },\n fields: fieldsWithValueJoined,\n }\n\n if (isEditing) {\n return apiClient.chat.patch<ApiResource<MessageResource>>({\n url: `/me/conversations/${conversationId}/messages/${messageId}`,\n data,\n })\n } else {\n return apiClient.chat.post<ApiResource<MessageResource>>({\n url: `/me/conversations/${conversationId}/messages`,\n data,\n })\n }\n },\n onMutate: async ({\n text,\n attachments,\n }: {\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n }) => {\n if (message && isEditing) {\n const optimisticMessage = optimisticallyUpdateMessage({\n conversationId,\n message,\n text,\n })\n\n return { message: optimisticMessage }\n }\n\n const optimisticMessage = optimisticallyCreateMessage({\n conversationId,\n message,\n text,\n attachments,\n currentPerson,\n })\n\n return { message: optimisticMessage }\n },\n onError: (error, variables, context) => {\n const { message: optimisticMessage } = context || {}\n // Add error to the optimistic message from the cache on error\n if (optimisticMessage) {\n const queryKey = getMessagesQueryKey({ conversation_id: conversationId })\n chatQueryClient.setQueryData(\n queryKey,\n (data: InfiniteData<ApiCollection<MessageResource>> | undefined) =>\n updateRecordInPagesData({\n data,\n record: optimisticMessage,\n processRecord: (_next, prev) => ({\n ...prev,\n error: error.message || 'Failed to send message',\n pending: false, // Mark as no longer pending\n }),\n })\n )\n }\n },\n onSuccess: (result: ApiResource<MessageResource>, variables, context) => {\n const { message: optimisticMessage } = context || {}\n const updatedMessage = result.data\n type QueryData = InfiniteData<ApiCollection<MessageResource>>\n const queryKey = getMessagesQueryKey({ conversation_id: conversationId })\n\n // First remove the optimistic message if it exists\n if (optimisticMessage) {\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n deleteRecordInPagesData({\n data,\n record: optimisticMessage,\n })\n )\n }\n\n // Then add the real message\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n updateOrCreateRecordInPagesData({\n data,\n record: updatedMessage,\n })\n )\n },\n })\n\n return mutation\n}\n\nexport function isTemporaryMessageId(messageId?: string | null): boolean {\n return !!messageId && messageId.endsWith('-temp')\n}\nexport function isNewMessage(message?: MessageResource): boolean {\n return !message?.id || isTemporaryMessageId(message.id)\n}\n\n/**\n * Generate a random UUID (v4) for idempotent keys.\n * Uses Math.random, which is not cryptographically secure.\n * An actual crypto library requires native dependencies.\n * This is OK for now since idempotent keys are not security-sensitive\n * or need to be guaranteed unique.\n * They are short lived and we use it in combination with the message's creator_id so\n * their impact is scoped only the current user.\n */\nfunction insecureUUID(): string {\n return 'xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx'\n .replace(/x/g, () => Math.floor(Math.random() * 16).toString(16))\n .replace(/N/g, () => (Math.floor(Math.random() * 4) + 8).toString(16))\n}\n"]}
|
|
@@ -9,6 +9,7 @@ interface BaseConversationEventData extends Record<string, unknown> {
|
|
|
9
9
|
last_message_author_name?: string;
|
|
10
10
|
last_message_created_at?: DateString;
|
|
11
11
|
last_message_sort_key?: string;
|
|
12
|
+
last_message_idempotent_key?: string | null;
|
|
12
13
|
last_message_text_preview?: string;
|
|
13
14
|
latest_read_message_sort_key?: string;
|
|
14
15
|
organization_id: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/conversation_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAE1F,KAAK,UAAU,GAAG,MAAM,CAAA;AACxB,UAAU,yBAA0B,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACjE,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAA;QACV,WAAW,EAAE,UAAU,CAAA;QACvB,UAAU,EAAE,UAAU,CAAA;QACtB,sBAAsB,CAAC,EAAE,MAAM,CAAA;QAC/B,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,uBAAuB,CAAC,EAAE,UAAU,CAAA;QACpC,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,4BAA4B,CAAC,EAAE,MAAM,CAAA;QACrC,eAAe,EAAE,MAAM,CAAA;QACvB,gBAAgB,EAAE,OAAO,CAAA;QACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,KAAK,EAAE,MAAM,CAAA;QACb,UAAU,EAAE,UAAU,CAAA;KACvB,CAAA;CACF;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,sBAAsB,CAAA;IAC7B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,sBAAsB,CAAA;IAC7B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,wBAAwB,CAAA;IAC/B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,KAAK,EAAE,mBAAmB,CAAA;IAC1B,IAAI,EAAE,yBAAyB,CAAA;CAChC"}
|
|
1
|
+
{"version":3,"file":"conversation_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/conversation_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAE1F,KAAK,UAAU,GAAG,MAAM,CAAA;AACxB,UAAU,yBAA0B,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACjE,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAA;QACV,WAAW,EAAE,UAAU,CAAA;QACvB,UAAU,EAAE,UAAU,CAAA;QACtB,sBAAsB,CAAC,EAAE,MAAM,CAAA;QAC/B,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,uBAAuB,CAAC,EAAE,UAAU,CAAA;QACpC,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,2BAA2B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC3C,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,4BAA4B,CAAC,EAAE,MAAM,CAAA;QACrC,eAAe,EAAE,MAAM,CAAA;QACvB,gBAAgB,EAAE,OAAO,CAAA;QACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,KAAK,EAAE,MAAM,CAAA;QACb,UAAU,EAAE,UAAU,CAAA;KACvB,CAAA;CACF;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,sBAAsB,CAAA;IAC7B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,sBAAsB,CAAA;IAC7B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,wBAAwB,CAAA;IAC/B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,KAAK,EAAE,mBAAmB,CAAA;IAC1B,IAAI,EAAE,yBAAyB,CAAA;CAChC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/conversation_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\n\ntype DateString = string\ninterface BaseConversationEventData extends Record<string, unknown> {\n data: {\n id: number\n archived_at: DateString\n created_at: DateString\n last_message_author_id?: number\n last_message_author_name?: string\n last_message_created_at?: DateString\n last_message_sort_key?: string\n last_message_text_preview?: string\n latest_read_message_sort_key?: string\n organization_id: number\n replies_disabled: boolean\n subtitle?: string\n title: string\n updated_at: DateString\n }\n}\n\nexport interface ConversationCreatedEvent extends CustomMessage {\n event: 'conversation.created'\n data: BaseConversationEventData\n}\n\nexport interface ConversationUpdatedEvent extends CustomMessage {\n event: 'conversation.updated'\n data: BaseConversationEventData\n}\n\nexport interface ConversationDeletedEvent extends CustomMessage {\n event: 'conversation.destroyed'\n data: BaseConversationEventData\n}\n\nexport interface ConversationReadEvent extends CustomMessage {\n event: 'conversation.read'\n data: BaseConversationEventData\n}\n"]}
|
|
1
|
+
{"version":3,"file":"conversation_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/conversation_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\n\ntype DateString = string\ninterface BaseConversationEventData extends Record<string, unknown> {\n data: {\n id: number\n archived_at: DateString\n created_at: DateString\n last_message_author_id?: number\n last_message_author_name?: string\n last_message_created_at?: DateString\n last_message_sort_key?: string\n last_message_idempotent_key?: string | null\n last_message_text_preview?: string\n latest_read_message_sort_key?: string\n organization_id: number\n replies_disabled: boolean\n subtitle?: string\n title: string\n updated_at: DateString\n }\n}\n\nexport interface ConversationCreatedEvent extends CustomMessage {\n event: 'conversation.created'\n data: BaseConversationEventData\n}\n\nexport interface ConversationUpdatedEvent extends CustomMessage {\n event: 'conversation.updated'\n data: BaseConversationEventData\n}\n\nexport interface ConversationDeletedEvent extends CustomMessage {\n event: 'conversation.destroyed'\n data: BaseConversationEventData\n}\n\nexport interface ConversationReadEvent extends CustomMessage {\n event: 'conversation.read'\n data: BaseConversationEventData\n}\n"]}
|
|
@@ -14,6 +14,7 @@ interface BaseMessageEventData extends Record<string, unknown> {
|
|
|
14
14
|
text_edited_at: string | null;
|
|
15
15
|
html: string;
|
|
16
16
|
attachments: DenormalizedAttachmentResource[];
|
|
17
|
+
idempotent_key?: string | null;
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
export interface MessageCreatedEvent extends CustomMessage {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/message_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAC1F,OAAO,EAAE,8BAA8B,EAAE,MAAM,+CAA+C,CAAA;AAE9F,UAAU,oBAAqB,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC5D,IAAI,EAAE;QACJ,aAAa,EAAE,MAAM,CAAA;QACrB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,eAAe,EAAE,MAAM,CAAA;QACvB,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;QAC9B,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;QAC7B,IAAI,EAAE,MAAM,CAAA;QACZ,WAAW,EAAE,8BAA8B,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"message_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/message_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAC1F,OAAO,EAAE,8BAA8B,EAAE,MAAM,+CAA+C,CAAA;AAE9F,UAAU,oBAAqB,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC5D,IAAI,EAAE;QACJ,aAAa,EAAE,MAAM,CAAA;QACrB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,eAAe,EAAE,MAAM,CAAA;QACvB,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;QAC9B,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;QAC7B,IAAI,EAAE,MAAM,CAAA;QACZ,WAAW,EAAE,8BAA8B,EAAE,CAAA;QAC7C,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAC/B,CAAA;CACF;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,KAAK,EAAE,iBAAiB,CAAA;IACxB,IAAI,EAAE,oBAAoB,CAAA;CAC3B;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,KAAK,EAAE,iBAAiB,CAAA;IACxB,IAAI,EAAE,oBAAoB,CAAA;CAC3B;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,KAAK,EAAE,mBAAmB,CAAA;IAC1B,IAAI,EAAE,oBAAoB,CAAA;CAC3B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/message_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\nimport { DenormalizedAttachmentResource } from '../resources/denormalized_attachment_resource'\n\ninterface BaseMessageEventData extends Record<string, unknown> {\n data: {\n author_avatar: string\n author_id: number\n author_name: string\n conversation_id: number\n created_at: string\n deleted_at: string | null\n organization_id: number | null\n sort_key: string\n text: string\n text_edited_at: string | null\n html: string\n attachments: DenormalizedAttachmentResource[]\n }\n}\n\nexport interface MessageCreatedEvent extends CustomMessage {\n event: 'message.created'\n data: BaseMessageEventData\n}\n\nexport interface MessageUpdatedEvent extends CustomMessage {\n event: 'message.updated'\n data: BaseMessageEventData\n}\n\nexport interface MessageDeletedEvent extends CustomMessage {\n event: 'message.destroyed'\n data: BaseMessageEventData\n}\n"]}
|
|
1
|
+
{"version":3,"file":"message_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/message_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\nimport { DenormalizedAttachmentResource } from '../resources/denormalized_attachment_resource'\n\ninterface BaseMessageEventData extends Record<string, unknown> {\n data: {\n author_avatar: string\n author_id: number\n author_name: string\n conversation_id: number\n created_at: string\n deleted_at: string | null\n organization_id: number | null\n sort_key: string\n text: string\n text_edited_at: string | null\n html: string\n attachments: DenormalizedAttachmentResource[]\n idempotent_key?: string | null\n }\n}\n\nexport interface MessageCreatedEvent extends CustomMessage {\n event: 'message.created'\n data: BaseMessageEventData\n}\n\nexport interface MessageUpdatedEvent extends CustomMessage {\n event: 'message.updated'\n data: BaseMessageEventData\n}\n\nexport interface MessageDeletedEvent extends CustomMessage {\n event: 'message.destroyed'\n data: BaseMessageEventData\n}\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance tracking utilities for client-side metrics (React Native)
|
|
3
|
+
*
|
|
4
|
+
* This module provides functionality to track performance metrics and send them
|
|
5
|
+
* to Datadog via the backend API for aggregation.
|
|
6
|
+
*/
|
|
7
|
+
import { ApiClient } from '../hooks';
|
|
8
|
+
/**
|
|
9
|
+
* Start tracking message creation performance
|
|
10
|
+
*/
|
|
11
|
+
export declare function startMessageCreationTracking(idempotentKey: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Complete message creation tracking when the message.created event is received
|
|
14
|
+
*/
|
|
15
|
+
export declare function completeMessageCreationTracking({ apiClient, idempotentKey, }: {
|
|
16
|
+
apiClient: ApiClient;
|
|
17
|
+
idempotentKey: string;
|
|
18
|
+
}): void;
|
|
19
|
+
/**
|
|
20
|
+
* Complete message creation tracking when the conversation.updated event is received
|
|
21
|
+
*/
|
|
22
|
+
export declare function completeMessageCreationConversationTracking({ apiClient, idempotentKey, }: {
|
|
23
|
+
apiClient: ApiClient;
|
|
24
|
+
idempotentKey: string;
|
|
25
|
+
}): void;
|
|
26
|
+
//# sourceMappingURL=performance_tracking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance_tracking.d.ts","sourceRoot":"","sources":["../../src/utils/performance_tracking.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAgBpC;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAMxE;AAED;;GAEG;AACH,wBAAgB,+BAA+B,CAAC,EAC9C,SAAS,EACT,aAAa,GACd,EAAE;IACD,SAAS,EAAE,SAAS,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;CACtB,GAAG,IAAI,CASP;AAED;;GAEG;AACH,wBAAgB,2CAA2C,CAAC,EAC1D,SAAS,EACT,aAAa,GACd,EAAE;IACD,SAAS,EAAE,SAAS,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;CACtB,GAAG,IAAI,CASP"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance tracking utilities for client-side metrics (React Native)
|
|
3
|
+
*
|
|
4
|
+
* This module provides functionality to track performance metrics and send them
|
|
5
|
+
* to Datadog via the backend API for aggregation.
|
|
6
|
+
*/
|
|
7
|
+
import DeviceInfo from 'react-native-device-info';
|
|
8
|
+
import { Platform } from 'react-native';
|
|
9
|
+
const appName = DeviceInfo.getApplicationName();
|
|
10
|
+
const pendingMetrics = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* Start tracking message creation performance
|
|
13
|
+
*/
|
|
14
|
+
export function startMessageCreationTracking(idempotentKey) {
|
|
15
|
+
if (!idempotentKey)
|
|
16
|
+
return;
|
|
17
|
+
const startTime = Date.now();
|
|
18
|
+
pendingMetrics.set(`message:${idempotentKey}`, { idempotentKey, startTime });
|
|
19
|
+
pendingMetrics.set(`conversation:${idempotentKey}`, { idempotentKey, startTime });
|
|
20
|
+
cleanupStaleMetrics();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Complete message creation tracking when the message.created event is received
|
|
24
|
+
*/
|
|
25
|
+
export function completeMessageCreationTracking({ apiClient, idempotentKey, }) {
|
|
26
|
+
if (!idempotentKey)
|
|
27
|
+
return;
|
|
28
|
+
const duration = durationForIdempotentKey(`message:${idempotentKey}`);
|
|
29
|
+
if (!duration)
|
|
30
|
+
return;
|
|
31
|
+
sendPerformanceMetric(apiClient, {
|
|
32
|
+
name: 'chat.message.creation.message_roundtrip_time',
|
|
33
|
+
value: duration,
|
|
34
|
+
unit: 'milliseconds',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Complete message creation tracking when the conversation.updated event is received
|
|
39
|
+
*/
|
|
40
|
+
export function completeMessageCreationConversationTracking({ apiClient, idempotentKey, }) {
|
|
41
|
+
if (!idempotentKey)
|
|
42
|
+
return;
|
|
43
|
+
const duration = durationForIdempotentKey(`conversation:${idempotentKey}`);
|
|
44
|
+
if (!duration)
|
|
45
|
+
return;
|
|
46
|
+
sendPerformanceMetric(apiClient, {
|
|
47
|
+
name: 'chat.message.creation.conversation_roundtrip_time',
|
|
48
|
+
value: duration,
|
|
49
|
+
unit: 'milliseconds',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function durationForIdempotentKey(idempotentKey) {
|
|
53
|
+
const metric = pendingMetrics.get(idempotentKey);
|
|
54
|
+
if (!metric)
|
|
55
|
+
return undefined;
|
|
56
|
+
pendingMetrics.delete(idempotentKey);
|
|
57
|
+
const endTime = Date.now();
|
|
58
|
+
return endTime - metric.startTime;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Send performance metric to backend for Datadog submission
|
|
62
|
+
*/
|
|
63
|
+
async function sendPerformanceMetric(apiClient, metric) {
|
|
64
|
+
try {
|
|
65
|
+
await apiClient.chat.post({
|
|
66
|
+
url: `/me/metrics`,
|
|
67
|
+
data: {
|
|
68
|
+
data: {
|
|
69
|
+
type: 'Metric',
|
|
70
|
+
attributes: {
|
|
71
|
+
metric: {
|
|
72
|
+
name: metric.name,
|
|
73
|
+
value: metric.value,
|
|
74
|
+
unit: metric.unit,
|
|
75
|
+
platform: Platform.OS,
|
|
76
|
+
app: appName,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
// Ignore errors
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Clean up any stale metrics (older than 5 minutes)
|
|
89
|
+
* This prevents memory leaks if messages fail to complete for any reason
|
|
90
|
+
*/
|
|
91
|
+
function cleanupStaleMetrics() {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
const staleThreshold = 5 * 60 * 1000; // 5 minutes in milliseconds
|
|
94
|
+
for (const [key, metric] of pendingMetrics.entries()) {
|
|
95
|
+
if (now - metric.startTime > staleThreshold) {
|
|
96
|
+
pendingMetrics.delete(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=performance_tracking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance_tracking.js","sourceRoot":"","sources":["../../src/utils/performance_tracking.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Performance tracking utilities for client-side metrics (React Native)\n *\n * This module provides functionality to track performance metrics and send them\n * to Datadog via the backend API for aggregation.\n */\n\nimport DeviceInfo from 'react-native-device-info'\nimport { Platform } from 'react-native'\nimport { ApiClient } from '../hooks'\nconst appName = DeviceInfo.getApplicationName()\n\ninterface PerformanceMetric {\n name: string\n value: number\n unit: 'milliseconds' | 'seconds' | 'count'\n}\n\ninterface PendingMessageMetric {\n idempotentKey: string\n startTime: number\n}\n\nconst pendingMetrics = new Map<string, PendingMessageMetric>()\n\n/**\n * Start tracking message creation performance\n */\nexport function startMessageCreationTracking(idempotentKey: string): void {\n if (!idempotentKey) return\n const startTime = Date.now()\n pendingMetrics.set(`message:${idempotentKey}`, { idempotentKey, startTime })\n pendingMetrics.set(`conversation:${idempotentKey}`, { idempotentKey, startTime })\n cleanupStaleMetrics()\n}\n\n/**\n * Complete message creation tracking when the message.created event is received\n */\nexport function completeMessageCreationTracking({\n apiClient,\n idempotentKey,\n}: {\n apiClient: ApiClient\n idempotentKey: string\n}): void {\n if (!idempotentKey) return\n const duration = durationForIdempotentKey(`message:${idempotentKey}`)\n if (!duration) return\n sendPerformanceMetric(apiClient, {\n name: 'chat.message.creation.message_roundtrip_time',\n value: duration,\n unit: 'milliseconds',\n })\n}\n\n/**\n * Complete message creation tracking when the conversation.updated event is received\n */\nexport function completeMessageCreationConversationTracking({\n apiClient,\n idempotentKey,\n}: {\n apiClient: ApiClient\n idempotentKey: string\n}): void {\n if (!idempotentKey) return\n const duration = durationForIdempotentKey(`conversation:${idempotentKey}`)\n if (!duration) return\n sendPerformanceMetric(apiClient, {\n name: 'chat.message.creation.conversation_roundtrip_time',\n value: duration,\n unit: 'milliseconds',\n })\n}\n\nfunction durationForIdempotentKey(idempotentKey: string): number | undefined {\n const metric = pendingMetrics.get(idempotentKey)\n if (!metric) return undefined\n pendingMetrics.delete(idempotentKey)\n const endTime = Date.now()\n return endTime - metric.startTime\n}\n\n/**\n * Send performance metric to backend for Datadog submission\n */\nasync function sendPerformanceMetric(\n apiClient: ApiClient,\n metric: PerformanceMetric\n): Promise<void> {\n try {\n await apiClient.chat.post({\n url: `/me/metrics`,\n data: {\n data: {\n type: 'Metric',\n attributes: {\n metric: {\n name: metric.name,\n value: metric.value,\n unit: metric.unit,\n platform: Platform.OS,\n app: appName,\n },\n },\n },\n },\n })\n } catch (e) {\n // Ignore errors\n }\n}\n\n/**\n * Clean up any stale metrics (older than 5 minutes)\n * This prevents memory leaks if messages fail to complete for any reason\n */\nfunction cleanupStaleMetrics(): void {\n const now = Date.now()\n const staleThreshold = 5 * 60 * 1000 // 5 minutes in milliseconds\n\n for (const [key, metric] of pendingMetrics.entries()) {\n if (now - metric.startTime > staleThreshold) {\n pendingMetrics.delete(key)\n }\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "3.12.2-rc.
|
|
3
|
+
"version": "3.12.2-rc.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"react-native-url-polyfill": "^2.0.0",
|
|
56
56
|
"typescript": "<5.6.0"
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "f77dac6f37aef497509a31ef5030a0ba94c9b758"
|
|
59
59
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useMemo, useState, Dispatch, SetStateAction, useCallback } from 'react'
|
|
1
|
+
import React, { useMemo, useState, Dispatch, SetStateAction, useCallback, useRef } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
StatusBar,
|
|
4
4
|
StyleSheet,
|
|
@@ -354,6 +354,13 @@ const LightboxModal = ({
|
|
|
354
354
|
Supports the image gallery layout and swipe functionality.
|
|
355
355
|
============================ */
|
|
356
356
|
|
|
357
|
+
// Use refs to avoid re-rendering FlatList callbacks...
|
|
358
|
+
const stableCurrentImageIndexRef = useRef(currentImageIndex)
|
|
359
|
+
const stableImageAttachmentsLengthRef = useRef(imageAttachments.length)
|
|
360
|
+
// ...while still ensuring they are updated with the latest values
|
|
361
|
+
stableCurrentImageIndexRef.current = currentImageIndex
|
|
362
|
+
stableImageAttachmentsLengthRef.current = imageAttachments.length
|
|
363
|
+
|
|
357
364
|
// Used in tandem with FlatList's initialScrollIndex to quickly calculate the position and size of each image before they load.
|
|
358
365
|
const getItemLayout = useCallback(
|
|
359
366
|
(_: unknown, index: number) => ({
|
|
@@ -366,22 +373,20 @@ const LightboxModal = ({
|
|
|
366
373
|
|
|
367
374
|
// Captures the current image's index after the FlatList finishes its scroll animation.
|
|
368
375
|
// Used in tandem with onViewableItemsChanged to ensure the final value for currentImageIndex is set.
|
|
369
|
-
const onMomentumScrollEnd = useCallback(
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
[currentImageIndex, imageAttachments.length]
|
|
384
|
-
)
|
|
376
|
+
const onMomentumScrollEnd = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
377
|
+
// Calculate the index of the image that is currently visible
|
|
378
|
+
const imageOffsetX = event.nativeEvent.contentOffset.x
|
|
379
|
+
const newImageIndex = Math.round(imageOffsetX / WINDOW_WIDTH)
|
|
380
|
+
|
|
381
|
+
// Check if the image index has changed and the FlatList didn't scroll past the first or last image
|
|
382
|
+
const didImageIndexChange = newImageIndex !== stableCurrentImageIndexRef.current
|
|
383
|
+
const isImageIndexWithinBounds =
|
|
384
|
+
newImageIndex >= 0 && newImageIndex < stableImageAttachmentsLengthRef.current
|
|
385
|
+
|
|
386
|
+
if (didImageIndexChange && isImageIndexWithinBounds) {
|
|
387
|
+
setCurrentImageIndex(newImageIndex)
|
|
388
|
+
}
|
|
389
|
+
}, [])
|
|
385
390
|
|
|
386
391
|
// Supplements onMomentumScrollEnd by capturing the current image's index while the FlatList is actively scrolling.
|
|
387
392
|
// Used in tandem with viewabilityConfig to trigger when the image is 50% visible in the window.
|
|
@@ -393,11 +398,11 @@ const LightboxModal = ({
|
|
|
393
398
|
const firstViewableItem = viewableItems[0]
|
|
394
399
|
const newIndex = firstViewableItem.index
|
|
395
400
|
|
|
396
|
-
if (newIndex !== null && newIndex !==
|
|
401
|
+
if (newIndex !== null && newIndex !== stableCurrentImageIndexRef.current) {
|
|
397
402
|
setCurrentImageIndex(newIndex)
|
|
398
403
|
}
|
|
399
404
|
},
|
|
400
|
-
[
|
|
405
|
+
[]
|
|
401
406
|
)
|
|
402
407
|
|
|
403
408
|
/* ============================
|
|
@@ -5,6 +5,9 @@ import { useQueryClient } from '@tanstack/react-query'
|
|
|
5
5
|
import { ApiResource, ConversationResource } from '../types'
|
|
6
6
|
import { getRequestQueryKey } from './use_suspense_api'
|
|
7
7
|
import { useCallback, useMemo } from 'react'
|
|
8
|
+
import { completeMessageCreationConversationTracking } from '../utils/performance_tracking'
|
|
9
|
+
import { useCurrentPerson } from './use_current_person'
|
|
10
|
+
import { useApiClient } from './use_api_client'
|
|
8
11
|
|
|
9
12
|
interface Props {
|
|
10
13
|
conversationId: number
|
|
@@ -13,14 +16,27 @@ interface Props {
|
|
|
13
16
|
export function useConversationJoltEvents({ conversationId }: Props) {
|
|
14
17
|
const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`)
|
|
15
18
|
const queryClient = useQueryClient()
|
|
19
|
+
const currentPerson = useCurrentPerson()
|
|
20
|
+
const apiClient = useApiClient()
|
|
16
21
|
const queryKey = useMemo(
|
|
17
22
|
() => getRequestQueryKey(getConversationRequestArgs({ conversation_id: conversationId })),
|
|
18
23
|
[conversationId]
|
|
19
24
|
)
|
|
20
25
|
|
|
21
|
-
const handleUpdatedConversation = useCallback(
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
const handleUpdatedConversation = useCallback(
|
|
27
|
+
(e: JoltConversationEvent) => {
|
|
28
|
+
const { last_message_idempotent_key, last_message_author_id } = e.data.data
|
|
29
|
+
|
|
30
|
+
if (last_message_idempotent_key && last_message_author_id === currentPerson.id) {
|
|
31
|
+
completeMessageCreationConversationTracking({
|
|
32
|
+
apiClient,
|
|
33
|
+
idempotentKey: last_message_idempotent_key,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
queryClient.invalidateQueries({ queryKey })
|
|
37
|
+
},
|
|
38
|
+
[queryClient, queryKey, currentPerson.id, apiClient]
|
|
39
|
+
)
|
|
24
40
|
|
|
25
41
|
const handleConversationRead = useCallback(
|
|
26
42
|
(e: JoltConversationEvent) => {
|
|
@@ -15,6 +15,8 @@ import { transformReactionEventDataToReactionCountResource } from '../utils/jolt
|
|
|
15
15
|
import { getMessagesRequestArgs } from '../utils/request/messages'
|
|
16
16
|
import { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'
|
|
17
17
|
import { isTemporaryMessageId } from './use_message_create_or_update'
|
|
18
|
+
import { completeMessageCreationTracking } from '../utils/performance_tracking'
|
|
19
|
+
import { useApiClient } from './use_api_client'
|
|
18
20
|
|
|
19
21
|
interface Props {
|
|
20
22
|
conversationId: number
|
|
@@ -26,6 +28,7 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
|
|
|
26
28
|
const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`)
|
|
27
29
|
const messagesRequestArgs = getMessagesRequestArgs({ conversation_id: conversationId })
|
|
28
30
|
const messagesQueryKey = getRequestQueryKey(messagesRequestArgs)
|
|
31
|
+
const apiClient = useApiClient()
|
|
29
32
|
|
|
30
33
|
const { addTypingEvent, removeTypingEventById, removeAllTypingEventsByAuthorId } =
|
|
31
34
|
useTypingStatusCache(conversationId)
|
|
@@ -39,6 +42,9 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
|
|
|
39
42
|
|
|
40
43
|
if (e.event === 'message.created' && data.author_id) {
|
|
41
44
|
removeAllTypingEventsByAuthorId(data.author_id)
|
|
45
|
+
if (data.idempotent_key && data.author_id === currentPerson.id) {
|
|
46
|
+
completeMessageCreationTracking({ apiClient, idempotentKey: data.idempotent_key })
|
|
47
|
+
}
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
queryClient.setQueryData<QueryData>(messagesQueryKey, prev => {
|
|
@@ -3,16 +3,28 @@ import { ConversationRequestArgs } from '../utils/request/conversation'
|
|
|
3
3
|
import { useConversationsCache } from './use_conversations_cache'
|
|
4
4
|
import { useCurrentPerson, useCurrentPersonCache } from './use_current_person'
|
|
5
5
|
import { useJoltChannel, useJoltEvent } from './use_jolt'
|
|
6
|
+
import { completeMessageCreationConversationTracking } from '../utils/performance_tracking'
|
|
7
|
+
import { useApiClient } from './use_api_client'
|
|
6
8
|
|
|
7
9
|
export function useConversationsJoltEvents(args?: Partial<ConversationRequestArgs>) {
|
|
8
10
|
const currentPerson = useCurrentPerson()
|
|
9
11
|
const joltChannel = useJoltChannel(`chat.people.${currentPerson.id}`)
|
|
10
12
|
const cache = useConversationsCache(args)
|
|
11
13
|
const currentPersonCache = useCurrentPersonCache()
|
|
14
|
+
const apiClient = useApiClient()
|
|
12
15
|
|
|
13
|
-
useJoltEvent(joltChannel, 'conversation.updated', (e: JoltConversationEvent) =>
|
|
16
|
+
useJoltEvent(joltChannel, 'conversation.updated', (e: JoltConversationEvent) => {
|
|
17
|
+
if (
|
|
18
|
+
e.data.data.last_message_idempotent_key &&
|
|
19
|
+
e.data.data.last_message_author_id === currentPerson.id
|
|
20
|
+
) {
|
|
21
|
+
completeMessageCreationConversationTracking({
|
|
22
|
+
apiClient,
|
|
23
|
+
idempotentKey: e.data.data.last_message_idempotent_key,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
14
26
|
cache.fetchUpdate({ id: e.data.data.id })
|
|
15
|
-
)
|
|
27
|
+
})
|
|
16
28
|
useJoltEvent(joltChannel, 'conversation.created', (e: JoltConversationEvent) =>
|
|
17
29
|
cache.fetchCreate({ id: e.data.data.id })
|
|
18
30
|
)
|
|
@@ -12,6 +12,7 @@ import { DenormalizedAttachmentResourceForCreate } from '../types/resources/deno
|
|
|
12
12
|
import { useCurrentPerson } from './use_current_person'
|
|
13
13
|
import { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message'
|
|
14
14
|
import { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message'
|
|
15
|
+
import { startMessageCreationTracking } from '../utils/performance_tracking'
|
|
15
16
|
|
|
16
17
|
interface Props {
|
|
17
18
|
conversationId: number
|
|
@@ -36,11 +37,17 @@ export function useMessageCreateOrUpdate({ conversationId, message }: Props) {
|
|
|
36
37
|
const fieldsWithValueJoined = Object.fromEntries(
|
|
37
38
|
Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])
|
|
38
39
|
)
|
|
40
|
+
let attributes: any = { text, ...(attachments ? { attachments } : {}) }
|
|
41
|
+
if (!isEditing) {
|
|
42
|
+
const idempotentKey = insecureUUID()
|
|
43
|
+
attributes.idempotent_key = idempotentKey
|
|
44
|
+
startMessageCreationTracking(idempotentKey)
|
|
45
|
+
}
|
|
39
46
|
const data = {
|
|
40
47
|
...requestParams.data,
|
|
41
48
|
data: {
|
|
42
49
|
type: 'Message',
|
|
43
|
-
attributes
|
|
50
|
+
attributes,
|
|
44
51
|
},
|
|
45
52
|
fields: fieldsWithValueJoined,
|
|
46
53
|
}
|
|
@@ -139,3 +146,18 @@ export function isTemporaryMessageId(messageId?: string | null): boolean {
|
|
|
139
146
|
export function isNewMessage(message?: MessageResource): boolean {
|
|
140
147
|
return !message?.id || isTemporaryMessageId(message.id)
|
|
141
148
|
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generate a random UUID (v4) for idempotent keys.
|
|
152
|
+
* Uses Math.random, which is not cryptographically secure.
|
|
153
|
+
* An actual crypto library requires native dependencies.
|
|
154
|
+
* This is OK for now since idempotent keys are not security-sensitive
|
|
155
|
+
* or need to be guaranteed unique.
|
|
156
|
+
* They are short lived and we use it in combination with the message's creator_id so
|
|
157
|
+
* their impact is scoped only the current user.
|
|
158
|
+
*/
|
|
159
|
+
function insecureUUID(): string {
|
|
160
|
+
return 'xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx'
|
|
161
|
+
.replace(/x/g, () => Math.floor(Math.random() * 16).toString(16))
|
|
162
|
+
.replace(/N/g, () => (Math.floor(Math.random() * 4) + 8).toString(16))
|
|
163
|
+
}
|
|
@@ -10,6 +10,7 @@ interface BaseConversationEventData extends Record<string, unknown> {
|
|
|
10
10
|
last_message_author_name?: string
|
|
11
11
|
last_message_created_at?: DateString
|
|
12
12
|
last_message_sort_key?: string
|
|
13
|
+
last_message_idempotent_key?: string | null
|
|
13
14
|
last_message_text_preview?: string
|
|
14
15
|
latest_read_message_sort_key?: string
|
|
15
16
|
organization_id: number
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance tracking utilities for client-side metrics (React Native)
|
|
3
|
+
*
|
|
4
|
+
* This module provides functionality to track performance metrics and send them
|
|
5
|
+
* to Datadog via the backend API for aggregation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import DeviceInfo from 'react-native-device-info'
|
|
9
|
+
import { Platform } from 'react-native'
|
|
10
|
+
import { ApiClient } from '../hooks'
|
|
11
|
+
const appName = DeviceInfo.getApplicationName()
|
|
12
|
+
|
|
13
|
+
interface PerformanceMetric {
|
|
14
|
+
name: string
|
|
15
|
+
value: number
|
|
16
|
+
unit: 'milliseconds' | 'seconds' | 'count'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface PendingMessageMetric {
|
|
20
|
+
idempotentKey: string
|
|
21
|
+
startTime: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pendingMetrics = new Map<string, PendingMessageMetric>()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Start tracking message creation performance
|
|
28
|
+
*/
|
|
29
|
+
export function startMessageCreationTracking(idempotentKey: string): void {
|
|
30
|
+
if (!idempotentKey) return
|
|
31
|
+
const startTime = Date.now()
|
|
32
|
+
pendingMetrics.set(`message:${idempotentKey}`, { idempotentKey, startTime })
|
|
33
|
+
pendingMetrics.set(`conversation:${idempotentKey}`, { idempotentKey, startTime })
|
|
34
|
+
cleanupStaleMetrics()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Complete message creation tracking when the message.created event is received
|
|
39
|
+
*/
|
|
40
|
+
export function completeMessageCreationTracking({
|
|
41
|
+
apiClient,
|
|
42
|
+
idempotentKey,
|
|
43
|
+
}: {
|
|
44
|
+
apiClient: ApiClient
|
|
45
|
+
idempotentKey: string
|
|
46
|
+
}): void {
|
|
47
|
+
if (!idempotentKey) return
|
|
48
|
+
const duration = durationForIdempotentKey(`message:${idempotentKey}`)
|
|
49
|
+
if (!duration) return
|
|
50
|
+
sendPerformanceMetric(apiClient, {
|
|
51
|
+
name: 'chat.message.creation.message_roundtrip_time',
|
|
52
|
+
value: duration,
|
|
53
|
+
unit: 'milliseconds',
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Complete message creation tracking when the conversation.updated event is received
|
|
59
|
+
*/
|
|
60
|
+
export function completeMessageCreationConversationTracking({
|
|
61
|
+
apiClient,
|
|
62
|
+
idempotentKey,
|
|
63
|
+
}: {
|
|
64
|
+
apiClient: ApiClient
|
|
65
|
+
idempotentKey: string
|
|
66
|
+
}): void {
|
|
67
|
+
if (!idempotentKey) return
|
|
68
|
+
const duration = durationForIdempotentKey(`conversation:${idempotentKey}`)
|
|
69
|
+
if (!duration) return
|
|
70
|
+
sendPerformanceMetric(apiClient, {
|
|
71
|
+
name: 'chat.message.creation.conversation_roundtrip_time',
|
|
72
|
+
value: duration,
|
|
73
|
+
unit: 'milliseconds',
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function durationForIdempotentKey(idempotentKey: string): number | undefined {
|
|
78
|
+
const metric = pendingMetrics.get(idempotentKey)
|
|
79
|
+
if (!metric) return undefined
|
|
80
|
+
pendingMetrics.delete(idempotentKey)
|
|
81
|
+
const endTime = Date.now()
|
|
82
|
+
return endTime - metric.startTime
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Send performance metric to backend for Datadog submission
|
|
87
|
+
*/
|
|
88
|
+
async function sendPerformanceMetric(
|
|
89
|
+
apiClient: ApiClient,
|
|
90
|
+
metric: PerformanceMetric
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
try {
|
|
93
|
+
await apiClient.chat.post({
|
|
94
|
+
url: `/me/metrics`,
|
|
95
|
+
data: {
|
|
96
|
+
data: {
|
|
97
|
+
type: 'Metric',
|
|
98
|
+
attributes: {
|
|
99
|
+
metric: {
|
|
100
|
+
name: metric.name,
|
|
101
|
+
value: metric.value,
|
|
102
|
+
unit: metric.unit,
|
|
103
|
+
platform: Platform.OS,
|
|
104
|
+
app: appName,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
} catch (e) {
|
|
111
|
+
// Ignore errors
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Clean up any stale metrics (older than 5 minutes)
|
|
117
|
+
* This prevents memory leaks if messages fail to complete for any reason
|
|
118
|
+
*/
|
|
119
|
+
function cleanupStaleMetrics(): void {
|
|
120
|
+
const now = Date.now()
|
|
121
|
+
const staleThreshold = 5 * 60 * 1000 // 5 minutes in milliseconds
|
|
122
|
+
|
|
123
|
+
for (const [key, metric] of pendingMetrics.entries()) {
|
|
124
|
+
if (now - metric.startTime > staleThreshold) {
|
|
125
|
+
pendingMetrics.delete(key)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|