@heystack/otel 0.16.0 → 0.18.0
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/README.md +54 -0
- package/dist/product.d.ts +77 -0
- package/dist/product.js +135 -0
- package/dist/web.d.ts +4 -0
- package/dist/web.js +9 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -659,3 +659,57 @@ claude mcp add --transport http heystack https://mcp.heystack.dev/mcp
|
|
|
659
659
|
```
|
|
660
660
|
|
|
661
661
|
One-click links for Cursor and VS Code, commands for Codex/Gemini CLI, personal access tokens for CI, and the full tool reference are in the docs: **https://heystack.dev/docs/mcp**.
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
## Product activity (0.17.0)
|
|
665
|
+
|
|
666
|
+
Use `createProductAnalytics` from `@heystack/otel/product` alongside the runtime
|
|
667
|
+
instrumentation. `track({ name, feature?, surface?, action?, outcome?, userId?,
|
|
668
|
+
sessionId?, traceId?, spanId?, properties? })` queues an explicit event.
|
|
669
|
+
`withContext(context)` returns an immutable request/session tracker;
|
|
670
|
+
`feature(name, options?)` and `screen(name, options?)` are convenience methods.
|
|
671
|
+
There is no mutable global user identity. Read the [full guide](https://heystack.dev/docs/product-activity).
|
|
672
|
+
|
|
673
|
+
```ts
|
|
674
|
+
import { createProductAnalytics } from "@heystack/otel/product";
|
|
675
|
+
const activity = createProductAnalytics({ apiKey, service: "api", version: "2.4.0", allowedProperties: ["plan"] });
|
|
676
|
+
activity.withContext({ userId, sessionId, feature: "signup" }).track({
|
|
677
|
+
name: "signup.completed", outcome: "succeeded", properties: { plan: "starter" },
|
|
678
|
+
});
|
|
679
|
+
await activity.flush(); // Workers: ctx.waitUntil(activity.flush())
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
Only allowlisted metadata is exported. Queue: 200 events; batches: 50;
|
|
683
|
+
properties: 20 keys, 256-character strings. `track` reports queued, `flush`
|
|
684
|
+
reports HTTP acceptance, not durable query visibility. Failed batches remain
|
|
685
|
+
bounded for retry; counts may include duplicates. No automatic click/form/text
|
|
686
|
+
capture. The browser-only `flushIntervalMs` timer is opt-in (minimum 5s);
|
|
687
|
+
servers have no background timer. Call `shutdown()` to stop and flush.
|
|
688
|
+
|
|
689
|
+
`instrumentWeb` now accepts `sessionId`, a UUID shared with product activity.
|
|
690
|
+
Capture product collection's original fetch before calling `instrumentWeb`,
|
|
691
|
+
or supply an original `fetch`. Rotate session UUID and discard bound user
|
|
692
|
+
trackers on logout; restart replay to avoid linking different people. Replay
|
|
693
|
+
sampling remains independent. Use a dedicated browser ingest configuration,
|
|
694
|
+
never expose the backend secret. Native clients may emit the documented event
|
|
695
|
+
attributes through existing log APIs.
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
### Product instrumentation 0.18.0
|
|
699
|
+
|
|
700
|
+
`createBrowserProductAnalytics` provides a per-browser identity/session facade:
|
|
701
|
+
`identify` rotates on account changes; `reset` clears identity and returns a fresh
|
|
702
|
+
session UUID. Restart replay with that UUID. Servers continue to use immutable
|
|
703
|
+
`createProductAnalytics().withContext()` per request.
|
|
704
|
+
|
|
705
|
+
Events accept `eventId` (stable on retries) and `attemptId` (shared by started and
|
|
706
|
+
terminal outcomes). Optional `eventDefinitions` validates declared names and
|
|
707
|
+
required context. `allowUserProfiles` enables explicitly supplied
|
|
708
|
+
`userProfile: { displayName, source: "clerk" | "app" }`; `allowNetworkContext`
|
|
709
|
+
enables original-client IP/geography supplied via `network`. Both are off by
|
|
710
|
+
default. Heystack does not resolve monitored users through its own Clerk directory.
|
|
711
|
+
|
|
712
|
+
Emit `screen.viewed` for actual navigation, with stable `surface` and `sessionId`.
|
|
713
|
+
Emit feature attempts with `feature`, `attemptId` and `outcome`. Keep the same
|
|
714
|
+
user/session/attempt context across backend calls. See the [exact telemetry
|
|
715
|
+
contract and verification checklist](https://heystack.dev/docs/product-activity).
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type HeystackOptions } from "./core.js";
|
|
2
|
+
export interface ActivityContext {
|
|
3
|
+
/** Stable pseudonymous identifiers; never email addresses or credentials. */
|
|
4
|
+
userId?: string;
|
|
5
|
+
/** Optional display context, exported only with allowUserProfiles. */
|
|
6
|
+
userProfile?: {
|
|
7
|
+
displayName: string;
|
|
8
|
+
source: "app" | "clerk";
|
|
9
|
+
};
|
|
10
|
+
/** Observed client network context, never inferred from an exporter server. */
|
|
11
|
+
network?: {
|
|
12
|
+
ip?: string;
|
|
13
|
+
country?: string;
|
|
14
|
+
region?: string;
|
|
15
|
+
city?: string;
|
|
16
|
+
source?: string;
|
|
17
|
+
};
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
feature?: string;
|
|
20
|
+
/** Stable screen/route name, not a URL containing customer data. */
|
|
21
|
+
surface?: string;
|
|
22
|
+
traceId?: string;
|
|
23
|
+
spanId?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ProductEvent extends ActivityContext {
|
|
26
|
+
name: string;
|
|
27
|
+
eventId?: string;
|
|
28
|
+
attemptId?: string;
|
|
29
|
+
action?: string;
|
|
30
|
+
outcome?: "started" | "succeeded" | "failed" | "cancelled";
|
|
31
|
+
properties?: Record<string, string | number | boolean>;
|
|
32
|
+
}
|
|
33
|
+
export interface ProductAnalyticsOptions extends HeystackOptions {
|
|
34
|
+
environment?: string;
|
|
35
|
+
allowUserProfiles?: boolean;
|
|
36
|
+
allowNetworkContext?: boolean;
|
|
37
|
+
/** Optional versioned event catalogue: rejects unlisted names or missing context. */
|
|
38
|
+
eventDefinitions?: Readonly<Record<string, {
|
|
39
|
+
required?: readonly ("feature" | "surface" | "sessionId" | "userId" | "attemptId" | "outcome")[];
|
|
40
|
+
}>>;
|
|
41
|
+
/** Supply an original, uninstrumented fetch if browser fetch is already patched. */
|
|
42
|
+
fetch?: typeof fetch;
|
|
43
|
+
/** Explicitly allow metadata keys. Unlisted properties are discarded. */
|
|
44
|
+
allowedProperties?: readonly string[];
|
|
45
|
+
/** Opt-in browser flush timer. Default: no background work; await flush on servers. */
|
|
46
|
+
flushIntervalMs?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface ActivityTracker {
|
|
49
|
+
track(event: ProductEvent): boolean;
|
|
50
|
+
feature(name: string, event?: Omit<ProductEvent, "name" | "feature">): boolean;
|
|
51
|
+
screen(name: string, event?: Omit<ProductEvent, "name" | "surface">): boolean;
|
|
52
|
+
/** Immutable binding. Create one per request/session; never store request identity globally. */
|
|
53
|
+
withContext(context: ActivityContext): ActivityTracker;
|
|
54
|
+
}
|
|
55
|
+
export interface ProductAnalytics extends ActivityTracker {
|
|
56
|
+
flush(): Promise<boolean>;
|
|
57
|
+
shutdown(): Promise<boolean>;
|
|
58
|
+
}
|
|
59
|
+
/** Explicit product activity over the existing OTLP logs ingest path.
|
|
60
|
+
* Bounded, best-effort collection; never auto-captures UI text or form contents.
|
|
61
|
+
* The return value means queued, not delivered or verified business success. */
|
|
62
|
+
export declare function createProductAnalytics(options: ProductAnalyticsOptions): ProductAnalytics;
|
|
63
|
+
/** Per-browser-session facade. Do not share this mutable identity facade on servers. */
|
|
64
|
+
export declare function createBrowserProductAnalytics(options: ProductAnalyticsOptions & {
|
|
65
|
+
sessionId?: string;
|
|
66
|
+
}): {
|
|
67
|
+
readonly sessionId: string;
|
|
68
|
+
/** Starts a fresh session on identity changes. Reconfigure replay with returned ID. */
|
|
69
|
+
identify(userId: string, profile?: ActivityContext["userProfile"]): string;
|
|
70
|
+
/** Logout boundary: clear identity and rotate session. Reconfigure replay too. */
|
|
71
|
+
reset(): string;
|
|
72
|
+
track(event: ProductEvent): boolean;
|
|
73
|
+
screen(surface: string, event?: Omit<ProductEvent, "name" | "surface">): boolean;
|
|
74
|
+
feature(feature: string, event?: Omit<ProductEvent, "name" | "feature">): boolean;
|
|
75
|
+
flush: () => Promise<boolean>;
|
|
76
|
+
shutdown: () => Promise<boolean>;
|
|
77
|
+
};
|
package/dist/product.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { suppressTracing } from "@opentelemetry/core";
|
|
2
|
+
import { context, trace, isSpanContextValid } from "@opentelemetry/api";
|
|
3
|
+
import { DEFAULT_ENDPOINT, releaseResourceAttributes } from "./core.js";
|
|
4
|
+
const MAX_QUEUE = 200;
|
|
5
|
+
const MAX_BATCH = 50;
|
|
6
|
+
const NAME = /^[a-zA-Z][a-zA-Z0-9_.-]{0,127}$/;
|
|
7
|
+
const SENSITIVE = /password|secret|token|authorization|cookie|email|phone|credit|card|body|prompt|content/i;
|
|
8
|
+
const text = (v, max = 256) => typeof v === "string" ? v.trim().slice(0, max) : "";
|
|
9
|
+
const id = (v, length) => typeof v === "string" && new RegExp(`^[a-f0-9]{${length}}$`, "i").test(v) && !/^0+$/.test(v) ? v.toLowerCase() : "";
|
|
10
|
+
/** Explicit product activity over the existing OTLP logs ingest path.
|
|
11
|
+
* Bounded, best-effort collection; never auto-captures UI text or form contents.
|
|
12
|
+
* The return value means queued, not delivered or verified business success. */
|
|
13
|
+
export function createProductAnalytics(options) {
|
|
14
|
+
const send = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
15
|
+
const endpoint = `${(options.endpoint ?? DEFAULT_ENDPOINT).replace(/\/+$/, "")}/v1/logs`;
|
|
16
|
+
const resource = { "service.name": options.service, ...releaseResourceAttributes(options),
|
|
17
|
+
...(options.environment ? { "deployment.environment": text(options.environment) } : {}) };
|
|
18
|
+
const allowed = new Set((options.allowedProperties ?? []).filter(k => NAME.test(k) && !SENSITIVE.test(k)).slice(0, 20));
|
|
19
|
+
const kv = (key, value) => ({ key, value: typeof value === "boolean" ? { boolValue: value } : typeof value === "number" ? { doubleValue: value } : { stringValue: value } });
|
|
20
|
+
let queue = [];
|
|
21
|
+
let sequence = 0;
|
|
22
|
+
let stopped = false;
|
|
23
|
+
let inFlight;
|
|
24
|
+
let timer;
|
|
25
|
+
const tracker = (bound) => ({
|
|
26
|
+
track(event) {
|
|
27
|
+
if (stopped || queue.length >= MAX_QUEUE || !NAME.test(event.name))
|
|
28
|
+
return false;
|
|
29
|
+
const ctx = { ...bound, ...event };
|
|
30
|
+
if (options.eventDefinitions && (!Object.hasOwn(options.eventDefinitions, event.name) ||
|
|
31
|
+
options.eventDefinitions[event.name]?.required?.some(key => !text(ctx[key]))))
|
|
32
|
+
return false;
|
|
33
|
+
const active = trace.getSpan(context.active())?.spanContext();
|
|
34
|
+
const attrs = {
|
|
35
|
+
"event.name": event.name, "event.domain": "product", "event.id": text(event.eventId) || crypto.randomUUID(), "heystack.schema.version": "1",
|
|
36
|
+
};
|
|
37
|
+
for (const [key, v] of Object.entries({ "enduser.id": ctx.userId, "session.id": ctx.sessionId,
|
|
38
|
+
"heystack.attempt.id": event.attemptId, "heystack.feature": ctx.feature, "heystack.surface": ctx.surface, "heystack.action": event.action, "heystack.outcome": event.outcome })) {
|
|
39
|
+
if (text(v))
|
|
40
|
+
attrs[key] = text(v);
|
|
41
|
+
}
|
|
42
|
+
if (options.allowUserProfiles && text(ctx.userId) && ctx.userProfile) {
|
|
43
|
+
attrs["heystack.user.display_name"] = text(ctx.userProfile.displayName);
|
|
44
|
+
attrs["heystack.user.source"] = ctx.userProfile.source === "clerk" ? "clerk" : "app";
|
|
45
|
+
}
|
|
46
|
+
if (options.allowNetworkContext && ctx.network) {
|
|
47
|
+
for (const [key, v] of Object.entries({ "client.address": ctx.network.ip, "geo.country.code": ctx.network.country,
|
|
48
|
+
"geo.locality.region": ctx.network.region, "geo.locality.name": ctx.network.city, "heystack.geo.source": ctx.network.source })) {
|
|
49
|
+
if (text(v))
|
|
50
|
+
attrs[key] = text(v);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const key of allowed) {
|
|
54
|
+
const v = event.properties?.[key];
|
|
55
|
+
if (typeof v === "string")
|
|
56
|
+
attrs[`heystack.properties.${key}`] = v.slice(0, 256);
|
|
57
|
+
else if (typeof v === "boolean" || (typeof v === "number" && Number.isFinite(v)))
|
|
58
|
+
attrs[`heystack.properties.${key}`] = v;
|
|
59
|
+
}
|
|
60
|
+
const traceId = ctx.traceId !== undefined ? id(ctx.traceId, 32) : (active && isSpanContextValid(active) ? active.traceId : "");
|
|
61
|
+
const spanId = ctx.traceId ? id(ctx.spanId, 16) : active && isSpanContextValid(active) ? active.spanId : "";
|
|
62
|
+
queue.push({ sequence: ++sequence, record: { timeUnixNano: (BigInt(Date.now()) * 1000000n).toString(),
|
|
63
|
+
severityText: event.outcome === "failed" ? "ERROR" : "INFO", severityNumber: event.outcome === "failed" ? 17 : 9,
|
|
64
|
+
body: { stringValue: event.name }, attributes: Object.entries(attrs).map(([k, v]) => kv(k, v)),
|
|
65
|
+
...(traceId ? { traceId, ...(spanId ? { spanId } : {}) } : {}) } });
|
|
66
|
+
return true;
|
|
67
|
+
},
|
|
68
|
+
feature(name, event = {}) { return this.track({ ...event, name: "feature.used", feature: name }); },
|
|
69
|
+
screen(name, event = {}) { return this.track({ ...event, name: "screen.viewed", surface: name }); },
|
|
70
|
+
withContext(next) { return tracker({ ...bound, ...next }); },
|
|
71
|
+
});
|
|
72
|
+
const flushThrough = (target) => {
|
|
73
|
+
// Every caller waits for its own sequence watermark, including events queued
|
|
74
|
+
// during an earlier export. New producers cannot extend this caller forever.
|
|
75
|
+
if (inFlight)
|
|
76
|
+
return inFlight.then(ok => ok ? flushThrough(target) : false);
|
|
77
|
+
const run = async () => {
|
|
78
|
+
while (queue.length && queue[0].sequence <= target) {
|
|
79
|
+
const size = Math.min(MAX_BATCH, queue.filter(item => item.sequence <= target).length);
|
|
80
|
+
const batch = queue.splice(0, size);
|
|
81
|
+
const records = batch.map(item => item.record);
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
84
|
+
try {
|
|
85
|
+
const response = await context.with(suppressTracing(context.active()), () => send(endpoint, { method: "POST", signal: controller.signal,
|
|
86
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${options.apiKey}` },
|
|
87
|
+
body: JSON.stringify({ resourceLogs: [{ resource: { attributes: Object.entries(resource).map(([k, v]) => kv(k, v)) },
|
|
88
|
+
scopeLogs: [{ scope: { name: "@heystack/otel/product", version: "1" }, logRecords: records }] }] }) }));
|
|
89
|
+
if (!response.ok)
|
|
90
|
+
throw new Error("export failed");
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
queue = [...batch, ...queue].slice(0, MAX_QUEUE);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
};
|
|
102
|
+
inFlight = run().finally(() => { inFlight = undefined; });
|
|
103
|
+
return inFlight;
|
|
104
|
+
};
|
|
105
|
+
const flush = () => flushThrough(sequence);
|
|
106
|
+
if (typeof window !== "undefined" && options.flushIntervalMs !== undefined) {
|
|
107
|
+
timer = setInterval(() => { void flush(); }, Math.max(5000, Number.isFinite(options.flushIntervalMs) ? options.flushIntervalMs : 5000));
|
|
108
|
+
}
|
|
109
|
+
return { ...tracker({}), flush, async shutdown() { stopped = true; if (timer)
|
|
110
|
+
clearInterval(timer); return flush(); } };
|
|
111
|
+
}
|
|
112
|
+
/** Per-browser-session facade. Do not share this mutable identity facade on servers. */
|
|
113
|
+
export function createBrowserProductAnalytics(options) {
|
|
114
|
+
const client = createProductAnalytics(options);
|
|
115
|
+
let sessionId = options.sessionId ?? crypto.randomUUID();
|
|
116
|
+
let user = {};
|
|
117
|
+
const bound = () => client.withContext({ ...user, sessionId });
|
|
118
|
+
return {
|
|
119
|
+
get sessionId() { return sessionId; },
|
|
120
|
+
/** Starts a fresh session on identity changes. Reconfigure replay with returned ID. */
|
|
121
|
+
identify(userId, profile) {
|
|
122
|
+
if (user.userId !== userId)
|
|
123
|
+
sessionId = crypto.randomUUID();
|
|
124
|
+
user = { userId, userProfile: profile };
|
|
125
|
+
return sessionId;
|
|
126
|
+
},
|
|
127
|
+
/** Logout boundary: clear identity and rotate session. Reconfigure replay too. */
|
|
128
|
+
reset() { user = {}; sessionId = crypto.randomUUID(); return sessionId; },
|
|
129
|
+
track(event) { return bound().track({ ...event, userId: user.userId, userProfile: user.userProfile, sessionId }); },
|
|
130
|
+
screen(surface, event = {}) { return this.track({ ...event, name: "screen.viewed", surface }); },
|
|
131
|
+
feature(feature, event = {}) { return this.track({ ...event, name: "feature.used", feature }); },
|
|
132
|
+
flush: client.flush,
|
|
133
|
+
shutdown: client.shutdown,
|
|
134
|
+
};
|
|
135
|
+
}
|
package/dist/web.d.ts
CHANGED
|
@@ -85,6 +85,8 @@ export interface InstrumentWebOptions {
|
|
|
85
85
|
* resource of exported browser logs for commit-level attribution.
|
|
86
86
|
*/
|
|
87
87
|
build?: string;
|
|
88
|
+
/** Optional shared UUID for product events, browser errors and replay. Rotate on logout. */
|
|
89
|
+
sessionId?: string;
|
|
88
90
|
/** Optional app-supplied user identifier stamped on the session. */
|
|
89
91
|
userId?: string;
|
|
90
92
|
/** Local overrides; by default sampling + masking come from server config. */
|
|
@@ -144,6 +146,8 @@ export declare function tracingActive(opts: Pick<InstrumentWebOptions, "tracing"
|
|
|
144
146
|
* (`cfg.errors_enabled`). **Fail-open**: an absent/`true` flag counts as enabled.
|
|
145
147
|
*/
|
|
146
148
|
export declare function errorCaptureActive(opts: Pick<InstrumentWebOptions, "errors">, cfg: Pick<RecorderConfig, "errors_enabled">): boolean;
|
|
149
|
+
/** A caller-owned session UUID allows product activity to correlate with replay. */
|
|
150
|
+
export declare function resolveWebSessionId(candidate?: string): string;
|
|
147
151
|
/** Entry point: fetch config, decide sampling, start rrweb, stream chunks.
|
|
148
152
|
* Returns a stop() function. Safe to call in any browser; no-ops on the server. */
|
|
149
153
|
export declare function instrumentWeb(opts: InstrumentWebOptions): Promise<() => void>;
|
package/dist/web.js
CHANGED
|
@@ -100,6 +100,14 @@ export function tracingActive(opts, cfg) {
|
|
|
100
100
|
export function errorCaptureActive(opts, cfg) {
|
|
101
101
|
return opts.errors !== false && cfg.errors_enabled !== false;
|
|
102
102
|
}
|
|
103
|
+
/** A caller-owned session UUID allows product activity to correlate with replay. */
|
|
104
|
+
export function resolveWebSessionId(candidate) {
|
|
105
|
+
if (candidate === undefined)
|
|
106
|
+
return crypto.randomUUID();
|
|
107
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(candidate))
|
|
108
|
+
throw new Error("sessionId must be a UUID");
|
|
109
|
+
return candidate;
|
|
110
|
+
}
|
|
103
111
|
/** Entry point: fetch config, decide sampling, start rrweb, stream chunks.
|
|
104
112
|
* Returns a stop() function. Safe to call in any browser; no-ops on the server. */
|
|
105
113
|
export async function instrumentWeb(opts) {
|
|
@@ -125,7 +133,7 @@ export async function instrumentWeb(opts) {
|
|
|
125
133
|
// One session id for the whole run — shared by errors, console logs AND replay
|
|
126
134
|
// (created here, before the replay sampling gate, so errors correlate even when
|
|
127
135
|
// replay isn't sampled; when it is, they share this id → error ↔ replay linking).
|
|
128
|
-
const sessionId =
|
|
136
|
+
const sessionId = resolveWebSessionId(opts.sessionId);
|
|
129
137
|
// service.version / service.build for release attribution on exported browser logs.
|
|
130
138
|
const releaseAttrs = releaseResourceAttributes({ version: opts.version, build: opts.build });
|
|
131
139
|
// 2. Browser distributed tracing (opt-in) — INDEPENDENT of replay sampling. Emits
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heystack/otel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Runtime-aware OpenTelemetry tracing that exports to Heystack (Node, Next.js, Workers).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"./web": {
|
|
31
31
|
"types": "./dist/web.d.ts",
|
|
32
32
|
"import": "./dist/web.js"
|
|
33
|
+
},
|
|
34
|
+
"./product": {
|
|
35
|
+
"types": "./dist/product.d.ts",
|
|
36
|
+
"import": "./dist/product.js"
|
|
33
37
|
}
|
|
34
38
|
},
|
|
35
39
|
"files": [
|