@adventurelabs/scout-core 1.0.69 → 1.0.71

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.
@@ -17,6 +17,7 @@ export function convertManualBoundingBoxToTag(boundingBox, event_id) {
17
17
  observation_type: "manual",
18
18
  class_name: newClassName,
19
19
  event_id: event_id,
20
+ location: null,
20
21
  };
21
22
  return newTag;
22
23
  }
@@ -1,11 +1,11 @@
1
- import { IEventWithTags, ITag } from "../types/db";
1
+ import { IEventAndTagsPrettyLocation, ITag } from "../types/db";
2
2
  import { IWebResponseCompatible } from "../types/requests";
3
3
  export declare function server_create_tags(tags: ITag[]): Promise<IWebResponseCompatible<ITag[]>>;
4
4
  export declare function server_delete_tags_by_ids(tag_ids: number[]): Promise<IWebResponseCompatible<boolean>>;
5
5
  export declare function server_update_tags(tags: ITag[]): Promise<IWebResponseCompatible<ITag[]>>;
6
- export declare function server_get_more_events_with_tags_by_herd(herd_id: number, offset: number, page_count?: number): Promise<IWebResponseCompatible<IEventWithTags[]>>;
7
- export declare function server_get_events_and_tags_for_device(device_id: number, limit?: number): Promise<IWebResponseCompatible<IEventWithTags[]>>;
6
+ export declare function server_get_more_events_with_tags_by_herd(herd_id: number, offset: number, page_count?: number): Promise<IWebResponseCompatible<IEventAndTagsPrettyLocation[]>>;
7
+ export declare function server_get_events_and_tags_for_device(device_id: number, limit?: number): Promise<IWebResponseCompatible<IEventAndTagsPrettyLocation[]>>;
8
8
  export declare function server_get_events_and_tags_for_devices_batch(device_ids: number[], limit?: number): Promise<IWebResponseCompatible<{
9
- [device_id: number]: IEventWithTags[];
9
+ [device_id: number]: IEventAndTagsPrettyLocation[];
10
10
  }>>;
11
- export declare function get_event_and_tags_by_event_id(event_id: number): Promise<IWebResponseCompatible<IEventWithTags>>;
11
+ export declare function get_event_and_tags_by_event_id(event_id: number): Promise<IWebResponseCompatible<IEventAndTagsPrettyLocation>>;
@@ -1,6 +1,57 @@
1
1
  "use server";
2
2
  // add tag to db
3
3
  import { newServerClient } from "../supabase/server";
4
+ // Helper functions to extract coordinates from location field
5
+ function extractLatitude(location) {
6
+ try {
7
+ if (typeof location === "object" && location !== null) {
8
+ if ("coordinates" in location && Array.isArray(location.coordinates)) {
9
+ return location.coordinates[1]; // latitude is second coordinate
10
+ }
11
+ if ("y" in location) {
12
+ return location.y;
13
+ }
14
+ }
15
+ if (typeof location === "string") {
16
+ const match = location.match(/Point\(([^)]+)\)/);
17
+ if (match) {
18
+ const coords = match[1].split(" ").map(Number);
19
+ if (coords.length === 2) {
20
+ return coords[1]; // latitude is second coordinate
21
+ }
22
+ }
23
+ }
24
+ }
25
+ catch (error) {
26
+ console.warn("Error extracting latitude:", error);
27
+ }
28
+ return null;
29
+ }
30
+ function extractLongitude(location) {
31
+ try {
32
+ if (typeof location === "object" && location !== null) {
33
+ if ("coordinates" in location && Array.isArray(location.coordinates)) {
34
+ return location.coordinates[0]; // longitude is first coordinate
35
+ }
36
+ if ("x" in location) {
37
+ return location.x;
38
+ }
39
+ }
40
+ if (typeof location === "string") {
41
+ const match = location.match(/Point\(([^)]+)\)/);
42
+ if (match) {
43
+ const coords = match[1].split(" ").map(Number);
44
+ if (coords.length === 2) {
45
+ return coords[0]; // longitude is first coordinate
46
+ }
47
+ }
48
+ }
49
+ }
50
+ catch (error) {
51
+ console.warn("Error extracting longitude:", error);
52
+ }
53
+ return null;
54
+ }
4
55
  import { EnumWebResponse, IWebResponse, } from "../types/requests";
5
56
  import { addSignedUrlsToEvents, addSignedUrlToEvent } from "./storage";
6
57
  export async function server_create_tags(tags) {
@@ -195,13 +246,13 @@ export async function server_get_events_and_tags_for_devices_batch(device_ids, l
195
246
  }
196
247
  export async function get_event_and_tags_by_event_id(event_id) {
197
248
  const supabase = await newServerClient();
198
- // use actual sql query to get event and tags instead of rpc
249
+ // Get event and tags
199
250
  const { data, error } = await supabase
200
251
  .from("events")
201
252
  .select(`
202
253
  *,
203
254
  tags: tags (*)
204
- `)
255
+ `)
205
256
  .eq("id", event_id);
206
257
  if (error) {
207
258
  console.warn("Error fetching event with tags by event id:", error.message);
@@ -218,6 +269,20 @@ export async function get_event_and_tags_by_event_id(event_id) {
218
269
  data: null,
219
270
  };
220
271
  }
272
+ // Get herd_id from device
273
+ const { data: deviceData, error: deviceError } = await supabase
274
+ .from("devices")
275
+ .select("herd_id")
276
+ .eq("id", data[0].device_id)
277
+ .single();
278
+ if (deviceError) {
279
+ console.warn("Error fetching device herd_id:", deviceError.message);
280
+ return {
281
+ status: EnumWebResponse.ERROR,
282
+ msg: deviceError.message,
283
+ data: null,
284
+ };
285
+ }
221
286
  // Transform location to latitude/longitude with better error handling
222
287
  let latitude = null;
223
288
  let longitude = null;
@@ -266,7 +331,22 @@ export async function get_event_and_tags_by_event_id(event_id) {
266
331
  device_id: data[0].device_id,
267
332
  timestamp_observation: data[0].timestamp_observation,
268
333
  is_public: data[0].is_public,
269
- tags: data[0].tags || [],
334
+ herd_id: deviceData.herd_id,
335
+ tags: (data[0].tags || []).map((tag) => ({
336
+ id: tag.id,
337
+ inserted_at: tag.inserted_at,
338
+ x: tag.x,
339
+ y: tag.y,
340
+ width: tag.width,
341
+ conf: tag.conf,
342
+ observation_type: tag.observation_type,
343
+ event_id: tag.event_id,
344
+ class_name: tag.class_name,
345
+ height: tag.height,
346
+ location: tag.location,
347
+ latitude: tag.location ? extractLatitude(tag.location) : null,
348
+ longitude: tag.location ? extractLongitude(tag.location) : null,
349
+ })),
270
350
  earthranger_url: data[0].earthranger_url,
271
351
  file_path: data[0].file_path,
272
352
  };
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export * from "./types/bounding_boxes";
11
11
  export * from "./types/events";
12
12
  export * from "./helpers/auth";
13
13
  export * from "./helpers/bounding_boxes";
14
+ export * from "./helpers/chat";
14
15
  export * from "./helpers/db";
15
16
  export * from "./helpers/devices";
16
17
  export * from "./helpers/email";
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ export * from "./types/events";
14
14
  // Helpers
15
15
  export * from "./helpers/auth";
16
16
  export * from "./helpers/bounding_boxes";
17
+ export * from "./helpers/chat";
17
18
  export * from "./helpers/db";
18
19
  export * from "./helpers/devices";
19
20
  export * from "./helpers/email";
@@ -52,18 +52,21 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
52
52
  file_path: string;
53
53
  id: number;
54
54
  session_id: number | null;
55
+ timestamp_observation: string | null;
55
56
  };
56
57
  Insert: {
57
58
  created_at?: string;
58
59
  file_path: string;
59
60
  id?: number;
60
61
  session_id?: number | null;
62
+ timestamp_observation?: string | null;
61
63
  };
62
64
  Update: {
63
65
  created_at?: string;
64
66
  file_path?: string;
65
67
  id?: number;
66
68
  session_id?: number | null;
69
+ timestamp_observation?: string | null;
67
70
  };
68
71
  Relationships: [{
69
72
  foreignKeyName: "artifacts_session_id_fkey";
@@ -465,6 +468,7 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
465
468
  height: number;
466
469
  id: number;
467
470
  inserted_at: string;
471
+ location: unknown | null;
468
472
  observation_type: Database["public"]["Enums"]["tag_observation_type"];
469
473
  width: number;
470
474
  x: number;
@@ -477,6 +481,7 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
477
481
  height?: number;
478
482
  id?: number;
479
483
  inserted_at?: string;
484
+ location?: unknown | null;
480
485
  observation_type: Database["public"]["Enums"]["tag_observation_type"];
481
486
  width: number;
482
487
  x: number;
@@ -489,6 +494,7 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
489
494
  height?: number;
490
495
  id?: number;
491
496
  inserted_at?: string;
497
+ location?: unknown | null;
492
498
  observation_type?: Database["public"]["Enums"]["tag_observation_type"];
493
499
  width?: number;
494
500
  x?: number;
@@ -890,7 +896,7 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
890
896
  device_id: number | null;
891
897
  timestamp_observation: string | null;
892
898
  is_public: boolean | null;
893
- tags: Database["public"]["Tables"]["tags"]["Row"][] | null;
899
+ tags: Database["public"]["CompositeTypes"]["tags_pretty_location"][] | null;
894
900
  herd_id: number | null;
895
901
  };
896
902
  event_plus_tags: {
@@ -941,6 +947,21 @@ export declare function useSupabase(): SupabaseClient<Database, "public", {
941
947
  distance_total: number | null;
942
948
  distance_max_from_start: number | null;
943
949
  };
950
+ tags_pretty_location: {
951
+ id: number | null;
952
+ inserted_at: string | null;
953
+ x: number | null;
954
+ y: number | null;
955
+ width: number | null;
956
+ conf: number | null;
957
+ observation_type: Database["public"]["Enums"]["tag_observation_type"] | null;
958
+ event_id: number | null;
959
+ class_name: string | null;
960
+ height: number | null;
961
+ location: unknown | null;
962
+ latitude: number | null;
963
+ longitude: number | null;
964
+ };
944
965
  zones_and_actions_pretty_location: {
945
966
  id: number | null;
946
967
  inserted_at: string | null;
@@ -43,18 +43,21 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
43
43
  file_path: string;
44
44
  id: number;
45
45
  session_id: number | null;
46
+ timestamp_observation: string | null;
46
47
  };
47
48
  Insert: {
48
49
  created_at?: string;
49
50
  file_path: string;
50
51
  id?: number;
51
52
  session_id?: number | null;
53
+ timestamp_observation?: string | null;
52
54
  };
53
55
  Update: {
54
56
  created_at?: string;
55
57
  file_path?: string;
56
58
  id?: number;
57
59
  session_id?: number | null;
60
+ timestamp_observation?: string | null;
58
61
  };
59
62
  Relationships: [{
60
63
  foreignKeyName: "artifacts_session_id_fkey";
@@ -456,6 +459,7 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
456
459
  height: number;
457
460
  id: number;
458
461
  inserted_at: string;
462
+ location: unknown | null;
459
463
  observation_type: Database["public"]["Enums"]["tag_observation_type"];
460
464
  width: number;
461
465
  x: number;
@@ -468,6 +472,7 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
468
472
  height?: number;
469
473
  id?: number;
470
474
  inserted_at?: string;
475
+ location?: unknown | null;
471
476
  observation_type: Database["public"]["Enums"]["tag_observation_type"];
472
477
  width: number;
473
478
  x: number;
@@ -480,6 +485,7 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
480
485
  height?: number;
481
486
  id?: number;
482
487
  inserted_at?: string;
488
+ location?: unknown | null;
483
489
  observation_type?: Database["public"]["Enums"]["tag_observation_type"];
484
490
  width?: number;
485
491
  x?: number;
@@ -881,7 +887,7 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
881
887
  device_id: number | null;
882
888
  timestamp_observation: string | null;
883
889
  is_public: boolean | null;
884
- tags: Database["public"]["Tables"]["tags"]["Row"][] | null;
890
+ tags: Database["public"]["CompositeTypes"]["tags_pretty_location"][] | null;
885
891
  herd_id: number | null;
886
892
  };
887
893
  event_plus_tags: {
@@ -932,6 +938,21 @@ export declare function newServerClient(): Promise<import("@supabase/supabase-js
932
938
  distance_total: number | null;
933
939
  distance_max_from_start: number | null;
934
940
  };
941
+ tags_pretty_location: {
942
+ id: number | null;
943
+ inserted_at: string | null;
944
+ x: number | null;
945
+ y: number | null;
946
+ width: number | null;
947
+ conf: number | null;
948
+ observation_type: Database["public"]["Enums"]["tag_observation_type"] | null;
949
+ event_id: number | null;
950
+ class_name: string | null;
951
+ height: number | null;
952
+ location: unknown | null;
953
+ latitude: number | null;
954
+ longitude: number | null;
955
+ };
935
956
  zones_and_actions_pretty_location: {
936
957
  id: number | null;
937
958
  inserted_at: string | null;
@@ -7,10 +7,11 @@ export type TagObservationType = Database["public"]["Enums"]["tag_observation_ty
7
7
  export type IUser = User;
8
8
  export type IDevice = Database["public"]["CompositeTypes"]["device_pretty_location"] & {
9
9
  api_keys_scout?: IApiKeyScout[];
10
- recent_events?: IEventWithTags[];
10
+ recent_events?: IEventAndTagsPrettyLocation[];
11
11
  };
12
12
  export type IEvent = Database["public"]["Tables"]["events"]["Row"];
13
13
  export type ITag = Database["public"]["Tables"]["tags"]["Row"];
14
+ export type ITagPrettyLocation = Database["public"]["CompositeTypes"]["tags_pretty_location"];
14
15
  export type IPlan = Database["public"]["Tables"]["plans"]["Row"];
15
16
  export type ILayer = Database["public"]["Tables"]["layers"]["Row"];
16
17
  export type IAction = Database["public"]["Tables"]["actions"]["Row"];
@@ -52,18 +52,21 @@ export type Database = {
52
52
  file_path: string;
53
53
  id: number;
54
54
  session_id: number | null;
55
+ timestamp_observation: string | null;
55
56
  };
56
57
  Insert: {
57
58
  created_at?: string;
58
59
  file_path: string;
59
60
  id?: number;
60
61
  session_id?: number | null;
62
+ timestamp_observation?: string | null;
61
63
  };
62
64
  Update: {
63
65
  created_at?: string;
64
66
  file_path?: string;
65
67
  id?: number;
66
68
  session_id?: number | null;
69
+ timestamp_observation?: string | null;
67
70
  };
68
71
  Relationships: [
69
72
  {
@@ -486,6 +489,7 @@ export type Database = {
486
489
  height: number;
487
490
  id: number;
488
491
  inserted_at: string;
492
+ location: unknown | null;
489
493
  observation_type: Database["public"]["Enums"]["tag_observation_type"];
490
494
  width: number;
491
495
  x: number;
@@ -498,6 +502,7 @@ export type Database = {
498
502
  height?: number;
499
503
  id?: number;
500
504
  inserted_at?: string;
505
+ location?: unknown | null;
501
506
  observation_type: Database["public"]["Enums"]["tag_observation_type"];
502
507
  width: number;
503
508
  x: number;
@@ -510,6 +515,7 @@ export type Database = {
510
515
  height?: number;
511
516
  id?: number;
512
517
  inserted_at?: string;
518
+ location?: unknown | null;
513
519
  observation_type?: Database["public"]["Enums"]["tag_observation_type"];
514
520
  width?: number;
515
521
  x?: number;
@@ -930,7 +936,7 @@ export type Database = {
930
936
  device_id: number | null;
931
937
  timestamp_observation: string | null;
932
938
  is_public: boolean | null;
933
- tags: Database["public"]["Tables"]["tags"]["Row"][] | null;
939
+ tags: Database["public"]["CompositeTypes"]["tags_pretty_location"][] | null;
934
940
  herd_id: number | null;
935
941
  };
936
942
  event_plus_tags: {
@@ -981,6 +987,21 @@ export type Database = {
981
987
  distance_total: number | null;
982
988
  distance_max_from_start: number | null;
983
989
  };
990
+ tags_pretty_location: {
991
+ id: number | null;
992
+ inserted_at: string | null;
993
+ x: number | null;
994
+ y: number | null;
995
+ width: number | null;
996
+ conf: number | null;
997
+ observation_type: Database["public"]["Enums"]["tag_observation_type"] | null;
998
+ event_id: number | null;
999
+ class_name: string | null;
1000
+ height: number | null;
1001
+ location: unknown | null;
1002
+ latitude: number | null;
1003
+ longitude: number | null;
1004
+ };
984
1005
  zones_and_actions_pretty_location: {
985
1006
  id: number | null;
986
1007
  inserted_at: string | null;
@@ -0,0 +1,135 @@
1
+ # Scout Core Helpers
2
+
3
+ ## Storage and Signed URLs
4
+
5
+ This module provides functionality for handling file storage and signed URL generation for media files.
6
+
7
+ ### Key Features
8
+
9
+ - **Signed URL Generation**: Generate secure, time-limited URLs for accessing media files
10
+ - **Backward Compatibility**: Support for both new signed URLs and legacy public URLs
11
+ - **Seamless Integration**: Signed URLs are automatically set as `media_url` for easy use
12
+ - **Efficient Client Usage**: Accepts existing Supabase client to avoid creating multiple instances
13
+
14
+ ### Files
15
+
16
+ - **`storage.ts`**: Server-side functions for generating signed URLs (requires "use server")
17
+ - **`eventUtils.ts`**: Client-side utility functions for working with event media URLs
18
+
19
+ ### Storage Functions (Server-side)
20
+
21
+ #### `generateSignedUrl(filePath, expiresIn, supabaseClient?)`
22
+
23
+ Generates a signed URL for a file in Supabase storage.
24
+
25
+ ```typescript
26
+ // With existing client
27
+ const signedUrl = await generateSignedUrl(
28
+ "events/123/image.jpg",
29
+ 3600,
30
+ supabaseClient
31
+ );
32
+
33
+ // Without existing client (creates new one)
34
+ const signedUrl = await generateSignedUrl("events/123/image.jpg", 3600);
35
+ ```
36
+
37
+ #### `addSignedUrlsToEvents(events, supabaseClient?)`
38
+
39
+ Adds signed URLs to an array of events, setting them as `media_url`.
40
+
41
+ ```typescript
42
+ // With existing client
43
+ const eventsWithUrls = await addSignedUrlsToEvents(events, supabaseClient);
44
+
45
+ // Without existing client (creates new one)
46
+ const eventsWithUrls = await addSignedUrlsToEvents(events);
47
+ ```
48
+
49
+ #### `addSignedUrlToEvent(event, supabaseClient?)`
50
+
51
+ Adds a signed URL to a single event, setting it as `media_url`.
52
+
53
+ ```typescript
54
+ // With existing client
55
+ const eventWithUrl = await addSignedUrlToEvent(event, supabaseClient);
56
+
57
+ // Without existing client (creates new one)
58
+ const eventWithUrl = await addSignedUrlToEvent(event);
59
+ ```
60
+
61
+ ### Event Utility Functions (Client-side)
62
+
63
+ #### `getEventMediaUrl(event)`
64
+
65
+ Gets the media URL for an event (now simply returns `event.media_url`).
66
+
67
+ ```typescript
68
+ const mediaUrl = getEventMediaUrl(event);
69
+ ```
70
+
71
+ #### `hasEventMedia(event)`
72
+
73
+ Checks if an event has any media URL available.
74
+
75
+ ```typescript
76
+ const hasMedia = hasEventMedia(event);
77
+ ```
78
+
79
+ ### Usage in Components
80
+
81
+ ```typescript
82
+ import { getEventMediaUrl, hasEventMedia } from "@adventurelabs/scout-core";
83
+
84
+ function EventMedia({ event }) {
85
+ const mediaUrl = getEventMediaUrl(event);
86
+
87
+ if (!hasEventMedia(event)) {
88
+ return <div>No media available</div>;
89
+ }
90
+
91
+ return <img src={mediaUrl} alt="Event media" />;
92
+ }
93
+ ```
94
+
95
+ ### Database Schema
96
+
97
+ Events now have two URL-related fields:
98
+
99
+ - `file_path`: The storage path for generating signed URLs
100
+ - `media_url`: The URL for accessing media (signed URL when file_path exists, legacy public URL otherwise)
101
+
102
+ ### How It Works
103
+
104
+ 1. **Event Creation**: Files are uploaded and `file_path` is stored (no `media_url` initially)
105
+ 2. **Event Fetching**: When events are retrieved, signed URLs are generated and set as `media_url`
106
+ 3. **URL Access**: Components simply use `event.media_url` as before
107
+ 4. **Fallback**: If signed URL generation fails, existing `media_url` is preserved
108
+
109
+ ### Performance Optimization
110
+
111
+ The storage functions accept an optional `supabaseClient` parameter to reuse existing client instances:
112
+
113
+ ```typescript
114
+ // Efficient: Reuse existing client
115
+ const supabase = await newServerClient();
116
+ const events = await fetchEvents(supabase);
117
+ const eventsWithUrls = await addSignedUrlsToEvents(events, supabase);
118
+
119
+ // Less efficient: Creates new client for each operation
120
+ const events = await fetchEvents();
121
+ const eventsWithUrls = await addSignedUrlsToEvents(events); // Creates new client
122
+ ```
123
+
124
+ ### Migration Strategy
125
+
126
+ 1. **Phase 1**: Store `file_path` for new events (no `media_url`)
127
+ 2. **Phase 2**: Generate signed URLs on fetch and set as `media_url`
128
+ 3. **Phase 3**: All events use signed URLs seamlessly
129
+
130
+ ### Security Benefits
131
+
132
+ - **Time-limited access**: URLs expire after a configurable time
133
+ - **Secure access**: URLs are cryptographically signed
134
+ - **No public exposure**: Files are not publicly accessible without signed URLs
135
+ - **Seamless integration**: No changes needed in existing components
@@ -0,0 +1,93 @@
1
+ # Scout Hooks
2
+
3
+ This directory contains React hooks for the Scout application.
4
+
5
+ ## useScoutRefresh
6
+
7
+ A hook for refreshing scout data with detailed timing measurements for performance monitoring and debugging.
8
+
9
+ ### Features
10
+
11
+ - **Automatic refresh**: Automatically refreshes data on component mount
12
+ - **Manual refresh**: Provides a function to manually trigger refreshes
13
+ - **Detailed timing**: Measures the duration of each portion of the loading process
14
+ - **Concurrent protection**: Prevents multiple simultaneous refresh operations
15
+ - **Error handling**: Graceful error handling with state consistency
16
+
17
+ ### Timing Measurements
18
+
19
+ The hook tracks the duration of several key operations:
20
+
21
+ 1. **Herd Modules API Call** (`herd_modules_api_duration_ms`): Time taken to fetch herd modules from the server
22
+ 2. **User API Call** (`user_api_duration_ms`): Time taken to fetch user data from the server
23
+ 3. **Data Processing** (`data_processing_duration_ms`): Time taken to process and dispatch data to the store
24
+ 4. **LocalStorage Operations** (`localStorage_duration_ms`): Time taken for localStorage read/write operations
25
+ 5. **Total Duration** (`herd_modules_loaded_in_ms`): Overall time from start to completion
26
+
27
+ ### Usage
28
+
29
+ ```tsx
30
+ import { useScoutRefresh } from "../hooks";
31
+
32
+ function MyComponent() {
33
+ const { handleRefresh, getTimingStats } = useScoutRefresh({
34
+ autoRefresh: true,
35
+ onRefreshComplete: () => {
36
+ console.log("Refresh completed!");
37
+ },
38
+ });
39
+
40
+ const handleManualRefresh = async () => {
41
+ await handleRefresh();
42
+
43
+ // Get timing statistics
44
+ const stats = getTimingStats();
45
+ console.log("Performance breakdown:");
46
+ console.log(`- Herd modules API: ${stats.herdModulesApi}ms`);
47
+ console.log(`- User API: ${stats.userApi}ms`);
48
+ console.log(`- Data processing: ${stats.dataProcessing}ms`);
49
+ console.log(`- LocalStorage: ${stats.localStorage}ms`);
50
+ console.log(`- Total: ${stats.totalDuration}ms`);
51
+ };
52
+
53
+ return (
54
+ <div>
55
+ <button onClick={handleManualRefresh}>Refresh Data</button>
56
+ </div>
57
+ );
58
+ }
59
+ ```
60
+
61
+ ### Redux State
62
+
63
+ The hook automatically updates the Redux store with timing information:
64
+
65
+ ```tsx
66
+ // Access timing data from the store
67
+ const timingData = useSelector((state) => ({
68
+ herdModulesApi: state.scout.herd_modules_api_duration_ms,
69
+ userApi: state.scout.user_api_duration_ms,
70
+ dataProcessing: state.scout.data_processing_duration_ms,
71
+ localStorage: state.scout.localStorage_duration_ms,
72
+ total: state.scout.herd_modules_loaded_in_ms,
73
+ }));
74
+ ```
75
+
76
+ ### Performance Monitoring
77
+
78
+ Use these timing measurements to:
79
+
80
+ - Identify performance bottlenecks in the loading process
81
+ - Monitor API response times
82
+ - Track data processing efficiency
83
+ - Debug localStorage performance issues
84
+ - Set performance budgets and alerts
85
+
86
+ ### Error Handling
87
+
88
+ The hook includes comprehensive error handling:
89
+
90
+ - API response validation
91
+ - Graceful fallbacks for localStorage failures
92
+ - Consistent state updates even on errors
93
+ - Detailed error logging for debugging
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "1.0.69",
3
+ "version": "1.0.71",
4
4
  "description": "Core utilities and helpers for Adventure Labs Scout applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -33,9 +33,9 @@
33
33
  },
34
34
  "homepage": "https://github.com/Adventurous-Bytes/scout#readme",
35
35
  "peerDependencies": {
36
+ "next": ">=15.0.0",
36
37
  "react": ">=18.0.0",
37
38
  "react-dom": ">=18.0.0",
38
- "next": ">=15.0.0",
39
39
  "react-redux": ">=9.0.0"
40
40
  },
41
41
  "devDependencies": {
@@ -44,9 +44,9 @@
44
44
  "typescript": "^5.0.0"
45
45
  },
46
46
  "dependencies": {
47
- "@supabase/supabase-js": "^2.53.0",
48
- "@supabase/ssr": "^0.6.1",
49
47
  "@reduxjs/toolkit": "^2.0.0",
48
+ "@supabase/ssr": "^0.6.1",
49
+ "@supabase/supabase-js": "^2.53.0",
50
50
  "zustand": "^4.0.0"
51
51
  }
52
52
  }