@idos-network/instrumentation 0.0.0-stage → 2.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 idOS Network
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,125 @@
1
- # Temporary Holding Version
1
+ # `@idos-network/instrumentation`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ OpenTelemetry instrumentation for the idOS JavaScript SDK, modelled on
4
+ [`@opentelemetry/instrumentation-pg`](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-pg).
5
+
6
+ Patches the SDK's public methods so that every idOS operation shows up as a span,
7
+ with the kwil round-trips it triggers nested underneath.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ pnpm add @idos-network/instrumentation @opentelemetry/api
13
+ ```
14
+
15
+ ## Setup
16
+
17
+ Every target is a class method, which lives on a prototype or a constructor —
18
+ ordinary mutable objects. So patching needs no module loader hook: import the
19
+ idOS module and hand its namespace over, once per package you use.
20
+
21
+ ```ts
22
+ import * as issuer from "@idos-network/issuer";
23
+ import * as kwil from "@idos-network/kwil-infra";
24
+ import { IdosInstrumentation } from "@idos-network/instrumentation";
25
+
26
+ const instrumentation = new IdosInstrumentation();
27
+ instrumentation.setTracerProvider(provider); // omit to use the global provider
28
+
29
+ instrumentation.patchModuleExports("@idos-network/issuer", issuer);
30
+ instrumentation.patchModuleExports("@idos-network/kwil-infra", kwil);
31
+ ```
32
+
33
+ This is the same call in Node and in the browser, and it survives bundling.
34
+ Patching a prototype also applies to instances that already exist, so there is
35
+ no race with `idOSIssuer.init()`.
36
+
37
+ In the browser, `new IdosInstrumentation({ requireParentSpan: true })` is worth
38
+ it: without it, an idOS call made outside a user-interaction span produces a
39
+ single-span orphan trace.
40
+
41
+ ### Automatic patching in Node (optional)
42
+
43
+ `InstrumentationBase` can also patch on import, with no app code at all. The
44
+ idOS packages are ESM-only, so that needs Node's loader hook — and two separate
45
+ things have to happen before the app imports any idOS package:
46
+
47
+ ```ts
48
+ // telemetry.mjs
49
+ import { register } from "node:module";
50
+ import { registerInstrumentations } from "@opentelemetry/instrumentation";
51
+ import { IdosInstrumentation } from "@idos-network/instrumentation";
52
+
53
+ register("@opentelemetry/instrumentation/hook.mjs", import.meta.url); // installs the loader
54
+ registerInstrumentations({ instrumentations: [new IdosInstrumentation()] }); // says what to patch
55
+ ```
56
+
57
+ ```sh
58
+ node --import ./telemetry.mjs ./app.mjs
59
+ ```
60
+
61
+ Worth it if you already launch with `--import`; otherwise prefer
62
+ `patchModuleExports`, which has no launch-flag to forget in a Dockerfile.
63
+
64
+ Two things that look like they should work and do not:
65
+
66
+ - `--import @opentelemetry/instrumentation/hook.mjs` on its own. `hook.mjs` is a
67
+ loader module exporting `resolve`/`load`/`initialize`, so importing it
68
+ registers nothing — it has to be passed to `register()`.
69
+ - `registerInstrumentations` called from inside your app. By then the idOS
70
+ modules are loaded and their namespaces are sealed.
71
+
72
+ ## What gets traced
73
+
74
+ | Module | Spans |
75
+ | -------------------------- | -------------------------------------------------------------------------------------------------- |
76
+ | `@idos-network/kwil-infra` | `KwilActionClient.call` / `.execute` — the idOS equivalent of `pg.Client.query` — and `.waitForTx` |
77
+ | `@idos-network/client` | `idOSClientConfiguration` / `Idle` / `WithUserSigner` / `LoggedIn` methods |
78
+ | `@idos-network/issuer` | `idOSIssuer` methods and `idOSIssuer.init` |
79
+ | `@idos-network/consumer` | `idOSConsumer` methods and `idOSConsumer.init` |
80
+
81
+ The enclave is not traced: it is local key material and iframe/MPC work, not
82
+ where request latency lives. `verifyCredential` is covered as
83
+ `idOSConsumer.verifyCredential`, which delegates straight to it.
84
+
85
+ Kwil spans carry `db.system.name=kwil`, `db.namespace=main` and
86
+ `db.operation.name=<action>`; `execute` also records `idos.kwil.tx_hash`. Every
87
+ span carries `idos.sdk.package`, `idos.sdk.class` and `idos.sdk.method`.
88
+
89
+ A synchronous `execute` broadcasts the transaction and then polls until it is
90
+ mined, which is usually where its latency goes. The wait is its own
91
+ `idos.kwil.waitForTx` child span, carrying `idos.kwil.tx_hash`, so broadcast time
92
+ and confirmation time show up separately. When it fails, `error.type` tells the
93
+ two outcomes apart: `KwilTxFailedError` (the chain rejected the transaction)
94
+ versus `KwilTxPollTimeoutError` (it was never mined inside the deadline).
95
+
96
+ The HTTP calls underneath are _not_ covered here — use
97
+ `@opentelemetry/instrumentation-undici` (Node) or
98
+ `@opentelemetry/instrumentation-fetch` (browser) alongside this package, and
99
+ they will nest under the spans above.
100
+
101
+ ## No arguments or return values are recorded
102
+
103
+ idOS SDK arguments and return values are credentials, wallet identifiers and
104
+ encryption keys. Nothing derived from them is put on a span by default — only
105
+ fixed identifiers such as the kwil action name. If you need more, opt in per
106
+ deployment with `requestHook` / `responseHook`:
107
+
108
+ ```ts
109
+ new IdosInstrumentation({
110
+ requestHook: (span, { className, methodName, args }) => {
111
+ if (className === "idOSConsumer" && methodName === "getCredentialSharedFromIDOS") {
112
+ span.setAttribute("idos.credential.id", String(args[0]));
113
+ }
114
+ },
115
+ });
116
+ ```
117
+
118
+ ## Config
119
+
120
+ | Option | Default | Meaning |
121
+ | ------------------- | ------- | ---------------------------------------------------- |
122
+ | `enabled` | `true` | Whether the instrumentation patches on registration. |
123
+ | `requireParentSpan` | `false` | Only create spans when one is already active. |
124
+ | `requestHook` | – | Add attributes from the call arguments. |
125
+ | `responseHook` | – | Add attributes from the resolved value. |
@@ -0,0 +1,144 @@
1
+ import { Attributes, Span, SpanKind } from "@opentelemetry/api";
2
+ import { InstrumentationBase, InstrumentationConfig, InstrumentationNodeModuleDefinition } from "@opentelemetry/instrumentation";
3
+ //#region src/types.d.ts
4
+ /** Data about the call that is about to be made, passed to {@link IdosInstrumentationConfig.requestHook}. */
5
+ type IdosRequestHookInformation = {
6
+ /** Module the traced method was exported from, e.g. `@idos-network/issuer`. */
7
+ moduleName: string;
8
+ /** Class the method lives on, or `undefined` for a bare function export. */
9
+ className: string | undefined;
10
+ /** Method name, e.g. `createUserProfile`. */
11
+ methodName: string;
12
+ /** The arguments the method was called with. May contain personal data — see README. */
13
+ args: readonly unknown[];
14
+ };
15
+ /** Data about the completed call, passed to {@link IdosInstrumentationConfig.responseHook}. */
16
+ type IdosResponseHookInformation = {
17
+ moduleName: string;
18
+ className: string | undefined;
19
+ methodName: string;
20
+ /** The resolved return value. May contain personal data — see README. */
21
+ result: unknown;
22
+ };
23
+ type IdosInstrumentationRequestHook = (span: Span, info: IdosRequestHookInformation) => void;
24
+ type IdosInstrumentationResponseHook = (span: Span, info: IdosResponseHookInformation) => void;
25
+ interface IdosInstrumentationConfig extends InstrumentationConfig {
26
+ /**
27
+ * Hook for adding custom attributes from the call arguments, or renaming the span.
28
+ *
29
+ * Nothing derived from arguments is recorded by default: idOS SDK arguments carry
30
+ * credentials, wallet identifiers and encryption keys. Opt in here, per deployment.
31
+ *
32
+ * @default undefined
33
+ */
34
+ requestHook?: IdosInstrumentationRequestHook;
35
+ /**
36
+ * Hook for adding custom attributes from the resolved return value.
37
+ *
38
+ * Same caveat as {@link requestHook}: return values carry credential contents.
39
+ *
40
+ * @default undefined
41
+ */
42
+ responseHook?: IdosInstrumentationResponseHook;
43
+ /**
44
+ * If true, spans are only created when there is already an active span.
45
+ *
46
+ * Useful in the browser, where an idOS call outside a user-interaction span
47
+ * would otherwise produce a single-span orphan trace.
48
+ *
49
+ * @default false
50
+ */
51
+ requireParentSpan?: boolean;
52
+ }
53
+ //#endregion
54
+ //#region src/instrumentation.d.ts
55
+ /**
56
+ * Traces the idOS SDK: every public method of `@idos-network/client`,
57
+ * `/consumer`, `/issuer`, the enclave providers, `verifyCredential`, and the
58
+ * kwil `call`/`execute` round-trips they all bottom out in.
59
+ */
60
+ export declare class IdosInstrumentation extends InstrumentationBase<IdosInstrumentationConfig> {
61
+ constructor(config?: IdosInstrumentationConfig);
62
+ protected init(): InstrumentationNodeModuleDefinition[];
63
+ /**
64
+ * Patch a module namespace you already imported.
65
+ *
66
+ * The automatic hooking in {@link InstrumentationBase} needs Node's module
67
+ * loader hook (`import-in-the-middle`), which has to be installed with
68
+ * `module.register()` before the import — and does not exist at all in a
69
+ * browser or a bundled app. This is the alternative everywhere: import the
70
+ * idOS module and hand its namespace over. Exports the module does not have
71
+ * are skipped.
72
+ */
73
+ patchModuleExports(moduleName: string, moduleExports: unknown): void;
74
+ /** Reverse of {@link patchModuleExports}. */
75
+ unpatchModuleExports(moduleName: string, moduleExports: unknown): void;
76
+ private findTarget;
77
+ private applyPatch;
78
+ private removePatch;
79
+ /** Resolves every target method to the object that owns it, skipping what is absent. */
80
+ private eachMember;
81
+ private traced;
82
+ private endSpan;
83
+ private failSpan;
84
+ }
85
+ //#endregion
86
+ //#region src/semconv.d.ts
87
+ /**
88
+ * Attributes emitted by this instrumentation that have no stable OpenTelemetry
89
+ * semantic convention. Stable ones (`db.*`, `error.type`) are imported from
90
+ * `@opentelemetry/semantic-conventions` at the call site.
91
+ */
92
+ /** Package the traced method lives in, e.g. `@idos-network/issuer`. */
93
+ export declare const ATTR_IDOS_SDK_PACKAGE = "idos.sdk.package";
94
+ /** Class the traced method lives on, e.g. `idOSIssuer`. */
95
+ export declare const ATTR_IDOS_SDK_CLASS = "idos.sdk.class";
96
+ /** Traced method name, e.g. `createUserProfile`. */
97
+ export declare const ATTR_IDOS_SDK_METHOD = "idos.sdk.method";
98
+ /** `db.system.name` value for the idOS kwil network. */
99
+ export declare const IDOS_KWIL_SYSTEM = "kwil";
100
+ /** Kwil namespace every idOS action runs against. */
101
+ export declare const IDOS_KWIL_NAMESPACE = "main";
102
+ /** Transaction hash returned by a kwil `execute`. */
103
+ export declare const ATTR_IDOS_KWIL_TX_HASH = "idos.kwil.tx_hash";
104
+ /** Whether a kwil `execute` waited for the transaction to be mined. */
105
+ export declare const ATTR_IDOS_KWIL_SYNCHRONOUS = "idos.kwil.synchronous";
106
+ //#endregion
107
+ //#region src/targets.d.ts
108
+ /** A method to wrap. A bare string uses every default. */
109
+ type MethodTarget = {
110
+ name: string;
111
+ /** Span name. Defaults to `<Class>.<method>`, or `<method>` for a function export. */
112
+ spanName?: (args: readonly unknown[]) => string;
113
+ /** Attributes derived from the arguments. Must not emit personal data. */
114
+ attributes?: (args: readonly unknown[]) => Attributes;
115
+ /** Attributes derived from the resolved value. Must not emit personal data. */
116
+ responseAttributes?: (result: unknown) => Attributes;
117
+ /** @default SpanKind.INTERNAL */
118
+ kind?: SpanKind;
119
+ };
120
+ type ClassTarget = {
121
+ /** Name of the class as exported by the module. */
122
+ className: string;
123
+ /** Instance methods, wrapped on the prototype. */
124
+ methods: (string | MethodTarget)[];
125
+ /** Static methods, wrapped on the constructor (`init`-style factories). */
126
+ staticMethods?: (string | MethodTarget)[];
127
+ };
128
+ type ModuleTarget = {
129
+ /** Module specifier. */
130
+ name: string;
131
+ supportedVersions: string[];
132
+ /**
133
+ * Only classes. Every method lives on a prototype or a constructor, both of
134
+ * which are ordinary mutable objects — that is what lets
135
+ * {@link IdosInstrumentation.patchModuleExports} work without a loader hook.
136
+ * A bare function export would sit on the sealed module namespace instead,
137
+ * and could only be reached via `import-in-the-middle`.
138
+ */
139
+ classes: ClassTarget[];
140
+ };
141
+ export declare const TARGETS: ModuleTarget[];
142
+ //#endregion
143
+ export type { ClassTarget, IdosInstrumentationConfig, IdosInstrumentationRequestHook, IdosInstrumentationResponseHook, IdosRequestHookInformation, IdosResponseHookInformation, MethodTarget, ModuleTarget };
144
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/instrumentation.ts","../src/semconv.ts","../src/targets.ts"],"mappings":";;;;KAIY;;EAEV;;EAEA;;EAEA;;EAEA;;;KAIU;EACV;EACA;EACA;;EAEA;;KAGU,kCAAkC,MAAM,MAAM,MAAM;KAEpD,mCACV,MAAM,MACN,MAAM;UAGS,kCAAkC;;;;;;;;;EASjD,cAAc;;;;;;;;EASd,eAAe;;;;;;;;;EAUf;;;;;;;;;qBChCW,4BAA4B,oBAAoB;EAC3D,YAAY,SAAQ;YAIV,QAAQ;;;;;;;;;;;EAsBlB,mBAAmB,oBAAoB;;EAMvC,qBAAqB,oBAAoB;UAKjC;UAMA;UAUA;;UAQA;UAmDA;UA+DA;UAmBA;;;;;;;;;;qBCvNG;;qBAGA;;qBAGA;;qBAGA;;qBAGA;;qBAGA;;qBAGA;;;;KCRD;EACV;;EAEA,YAAY;;EAEZ,cAAc,6BAA6B;;EAE3C,sBAAsB,oBAAoB;;EAE1C,OAAO;;KAGG;;EAEV;;EAEA,mBAAmB;;EAEnB,0BAA0B;;KAGhB;;EAEV;EACA;;;;;;;;EAQA,SAAS;;qBAyDE,SAAS"}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{SpanKind as e,SpanStatusCode as t,trace as n}from"@opentelemetry/api";import{InstrumentationBase as r,InstrumentationNodeModuleDefinition as i,isWrapped as a}from"@opentelemetry/instrumentation";import{ATTR_DB_NAMESPACE as o,ATTR_DB_OPERATION_NAME as s,ATTR_DB_SYSTEM_NAME as c,ATTR_ERROR_TYPE as l}from"@opentelemetry/semantic-conventions";const u=`idos.sdk.package`,d=`idos.sdk.class`,f=`idos.sdk.method`,p=`kwil`,m=`main`,h=`idos.kwil.tx_hash`,g=`idos.kwil.synchronous`,_=[`>=2.0.0 <3`],v=e=>{let t=e[0];return{[c]:p,[o]:m,...typeof t?.name==`string`?{[s]:t.name}:{}}},y=e=>t=>{let n=t[0];return typeof n?.name==`string`?`idos.kwil.${e} ${n.name}`:`idos.kwil.${e}`},b=[{name:`@idos-network/kwil-infra`,supportedVersions:_,classes:[{className:`KwilActionClient`,methods:[{name:`call`,kind:e.CLIENT,spanName:y(`call`),attributes:v},{name:`execute`,kind:e.CLIENT,spanName:y(`execute`),attributes:e=>({...v(e),[g]:e[2]===void 0||!!e[2]}),responseAttributes:e=>typeof e==`string`?{[h]:e}:{}},{name:`waitForTx`,kind:e.CLIENT,spanName:()=>`idos.kwil.waitForTx`,attributes:e=>({[c]:p,[o]:m,...typeof e[0]==`string`?{[h]:e[0]}:{}})}]}]},{name:`@idos-network/client`,supportedVersions:_,classes:[{className:`idOSClientConfiguration`,methods:[`createClient`]},{className:`idOSClientIdle`,methods:[`addressHasProfile`,`withUserSigner`,`logOut`],staticMethods:[`fromConfig`]},{className:`idOSClientWithUserSigner`,methods:[`hasProfile`,`createUserEncryptionProfile`,`logIn`,`logOut`]},{className:`idOSClientLoggedIn`,methods:[`requestDWGMessage`,`createCredential`,`removeCredential`,`getCredentialById`,`shareCredential`,`getAllCredentials`,`getAccessGrantsOwned`,`getAttributes`,`createAttribute`,`getCredentialContentSha256Hash`,`getCredentialContent`,`getCredentialWithEncryptedContent`,`getCredentialSharedContent`,`getGrants`,`getGrantsCount`,`getCredentialShared`,`revokeAccessGrant`,`addWallet`,`addWallets`,`getWallets`,`removeWallet`,`removeWallets`,`filterCredentials`,`requestAccessGrant`,`logOut`]}]},{name:`@idos-network/issuer`,supportedVersions:_,classes:[{className:`idOSIssuer`,staticMethods:[`init`],methods:[`hasProfile`,`createUserProfile`,`upsertWalletAsInserter`,`createUser`,`getUser`,`requestDelegatedWriteGrantMessage`,`createCredentialByDelegatedWriteGrant`,`editCredentialAsIssuer`,`getCredentialIdByContentHash`,`getCredentialShared`]}]},{name:`@idos-network/consumer`,supportedVersions:_,classes:[{className:`idOSConsumer`,staticMethods:[`init`],methods:[`getCredentialSharedFromIDOS`,`getCredentialSharedContentDecrypted`,`rescindSharedCredential`,`getGrantsCount`,`getAccessGrantsForCredential`,`getCredentialsSharedByUser`,`getAccessGrants`,`verifyCredential`]}]}],x=e=>typeof e?.then==`function`;var S=class extends r{constructor(e={}){super(`@idos-network/instrumentation`,`2.0.1`,e)}init(){return b.map(e=>new i(e.name,e.supportedVersions,t=>this.applyPatch(e.name,e,t),t=>this.removePatch(e.name,e,t)))}patchModuleExports(e,t){let n=this.findTarget(e);n&&this.applyPatch(e,n,t)}unpatchModuleExports(e,t){let n=this.findTarget(e);n&&this.removePatch(e,n,t)}findTarget(e){let t=b.find(t=>t.name===e);return t||this._diag.warn(`No instrumentation targets known for "${e}"`),t}applyPatch(e,t,n){return this.eachMember(e,t,n,(t,n,r,i)=>{a(t[n])&&this._unwrap(t,n),this._wrap(t,n,t=>this.traced(t,e,i,r))}),n}removePatch(e,t,n){return this.eachMember(e,t,n,(e,t)=>{a(e[t])&&this._unwrap(e,t)}),n}eachMember(e,t,n,r){if(typeof n!=`object`||!n)return;let i=n,a=(t,n,i)=>{let a=typeof n==`string`?{name:n}:n,o=`${i?`${i}.`:``}${a.name}`;if(typeof t[a.name]!=`function`){this._diag.debug(`${e}: ${o} is not a function, skipping`);return}try{r(t,a.name,a,i)}catch(t){this._diag.warn(`${e}: could not patch ${o}: ${String(t)}`)}};for(let n of t.classes){let t=i[n.className];if(typeof t!=`function`){this._diag.debug(`${e}: class ${n.className} not exported, skipping`);continue}let r=t.prototype;if(typeof r==`object`&&r)for(let e of n.methods)a(r,e,n.className);for(let e of n.staticMethods??[])a(t,e,n.className)}}traced(t,r,i,a){let o=this,s=`${i?`${i}.`:``}${a.name}`;return function(...c){let l=o.getConfig();if(l.requireParentSpan&&n.getActiveSpan()===void 0)return t.apply(this,c);let p={[u]:r,[f]:a.name,...i?{[d]:i}:{},...a.attributes?.(c)},m=a.spanName?.(c)??s,h={kind:a.kind??e.INTERNAL,attributes:p};return o.tracer.startActiveSpan(m,h,e=>{o._runSpanCustomizationHook(l.requestHook,`requestHook`,e,{moduleName:r,className:i,methodName:a.name,args:c});let n;try{n=t.apply(this,c)}catch(t){throw o.failSpan(e,t),t}return x(n)?n.then(t=>(o.endSpan(e,r,i,a,t),t),t=>{throw o.failSpan(e,t),t}):(o.endSpan(e,r,i,a,n),n)})}}endSpan(e,t,n,r,i){let a=r.responseAttributes?.(i);a&&e.setAttributes(a),this._runSpanCustomizationHook(this.getConfig().responseHook,`responseHook`,e,{moduleName:t,className:n,methodName:r.name,result:i}),e.end()}failSpan(e,n){n instanceof Error?(e.recordException(n),e.setAttribute(l,n.name),e.setStatus({code:t.ERROR,message:n.message})):(e.setAttribute(l,typeof n),e.setStatus({code:t.ERROR,message:String(n)})),e.end()}};export{g as ATTR_IDOS_KWIL_SYNCHRONOUS,h as ATTR_IDOS_KWIL_TX_HASH,d as ATTR_IDOS_SDK_CLASS,f as ATTR_IDOS_SDK_METHOD,u as ATTR_IDOS_SDK_PACKAGE,m as IDOS_KWIL_NAMESPACE,p as IDOS_KWIL_SYSTEM,S as IdosInstrumentation,b as TARGETS};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/semconv.ts","../src/targets.ts","../src/version.ts","../src/instrumentation.ts"],"sourcesContent":["/**\n * Attributes emitted by this instrumentation that have no stable OpenTelemetry\n * semantic convention. Stable ones (`db.*`, `error.type`) are imported from\n * `@opentelemetry/semantic-conventions` at the call site.\n */\n\n/** Package the traced method lives in, e.g. `@idos-network/issuer`. */\nexport const ATTR_IDOS_SDK_PACKAGE = \"idos.sdk.package\";\n\n/** Class the traced method lives on, e.g. `idOSIssuer`. */\nexport const ATTR_IDOS_SDK_CLASS = \"idos.sdk.class\";\n\n/** Traced method name, e.g. `createUserProfile`. */\nexport const ATTR_IDOS_SDK_METHOD = \"idos.sdk.method\";\n\n/** `db.system.name` value for the idOS kwil network. */\nexport const IDOS_KWIL_SYSTEM = \"kwil\";\n\n/** Kwil namespace every idOS action runs against. */\nexport const IDOS_KWIL_NAMESPACE = \"main\";\n\n/** Transaction hash returned by a kwil `execute`. */\nexport const ATTR_IDOS_KWIL_TX_HASH = \"idos.kwil.tx_hash\";\n\n/** Whether a kwil `execute` waited for the transaction to be mined. */\nexport const ATTR_IDOS_KWIL_SYNCHRONOUS = \"idos.kwil.synchronous\";\n","import type { Attributes } from \"@opentelemetry/api\";\n\nimport { SpanKind } from \"@opentelemetry/api\";\nimport {\n ATTR_DB_NAMESPACE,\n ATTR_DB_OPERATION_NAME,\n ATTR_DB_SYSTEM_NAME,\n} from \"@opentelemetry/semantic-conventions\";\n\nimport {\n ATTR_IDOS_KWIL_SYNCHRONOUS,\n ATTR_IDOS_KWIL_TX_HASH,\n IDOS_KWIL_NAMESPACE,\n IDOS_KWIL_SYSTEM,\n} from \"./semconv\";\n\n/** A method to wrap. A bare string uses every default. */\nexport type MethodTarget = {\n name: string;\n /** Span name. Defaults to `<Class>.<method>`, or `<method>` for a function export. */\n spanName?: (args: readonly unknown[]) => string;\n /** Attributes derived from the arguments. Must not emit personal data. */\n attributes?: (args: readonly unknown[]) => Attributes;\n /** Attributes derived from the resolved value. Must not emit personal data. */\n responseAttributes?: (result: unknown) => Attributes;\n /** @default SpanKind.INTERNAL */\n kind?: SpanKind;\n};\n\nexport type ClassTarget = {\n /** Name of the class as exported by the module. */\n className: string;\n /** Instance methods, wrapped on the prototype. */\n methods: (string | MethodTarget)[];\n /** Static methods, wrapped on the constructor (`init`-style factories). */\n staticMethods?: (string | MethodTarget)[];\n};\n\nexport type ModuleTarget = {\n /** Module specifier. */\n name: string;\n supportedVersions: string[];\n /**\n * Only classes. Every method lives on a prototype or a constructor, both of\n * which are ordinary mutable objects — that is what lets\n * {@link IdosInstrumentation.patchModuleExports} work without a loader hook.\n * A bare function export would sit on the sealed module namespace instead,\n * and could only be reached via `import-in-the-middle`.\n */\n classes: ClassTarget[];\n};\n\nconst SUPPORTED = [\">=2.0.0 <3\"];\n\n/**\n * Kwil `call`/`execute` are the idOS equivalent of `pg.Client.query`: every SDK\n * operation bottoms out here. The action name is a fixed identifier from the\n * action schema, so it is safe to record; the inputs are not.\n */\nconst kwilAttributes = (args: readonly unknown[]): Attributes => {\n const params = args[0] as { name?: unknown } | undefined;\n return {\n [ATTR_DB_SYSTEM_NAME]: IDOS_KWIL_SYSTEM,\n [ATTR_DB_NAMESPACE]: IDOS_KWIL_NAMESPACE,\n ...(typeof params?.name === \"string\" ? { [ATTR_DB_OPERATION_NAME]: params.name } : {}),\n };\n};\n\nconst kwilSpanName =\n (operation: string) =>\n (args: readonly unknown[]): string => {\n const params = args[0] as { name?: unknown } | undefined;\n return typeof params?.name === \"string\"\n ? `idos.kwil.${operation} ${params.name}`\n : `idos.kwil.${operation}`;\n };\n\n/** Methods shared by every logged-out-capable client state. */\nconst CLIENT_LOGGED_IN_METHODS = [\n \"requestDWGMessage\",\n \"createCredential\",\n \"removeCredential\",\n \"getCredentialById\",\n \"shareCredential\",\n \"getAllCredentials\",\n \"getAccessGrantsOwned\",\n \"getAttributes\",\n \"createAttribute\",\n \"getCredentialContentSha256Hash\",\n \"getCredentialContent\",\n \"getCredentialWithEncryptedContent\",\n \"getCredentialSharedContent\",\n \"getGrants\",\n \"getGrantsCount\",\n \"getCredentialShared\",\n \"revokeAccessGrant\",\n \"addWallet\",\n \"addWallets\",\n \"getWallets\",\n \"removeWallet\",\n \"removeWallets\",\n \"filterCredentials\",\n \"requestAccessGrant\",\n \"logOut\",\n];\n\nexport const TARGETS: ModuleTarget[] = [\n {\n name: \"@idos-network/kwil-infra\",\n supportedVersions: SUPPORTED,\n classes: [\n {\n className: \"KwilActionClient\",\n methods: [\n {\n name: \"call\",\n kind: SpanKind.CLIENT,\n spanName: kwilSpanName(\"call\"),\n attributes: kwilAttributes,\n },\n {\n name: \"execute\",\n kind: SpanKind.CLIENT,\n spanName: kwilSpanName(\"execute\"),\n attributes: (args) => ({\n ...kwilAttributes(args),\n // `synchronous` defaults to true and also decides whether the span\n // covers the tx-confirmation polling loop.\n [ATTR_IDOS_KWIL_SYNCHRONOUS]: args[2] === undefined ? true : Boolean(args[2]),\n }),\n responseAttributes: (result) =>\n typeof result === \"string\" ? { [ATTR_IDOS_KWIL_TX_HASH]: result } : {},\n },\n {\n // Usually the bulk of an `execute`: polling the node until the tx is\n // mined. A tx hash is a public chain identifier, so it is safe to record.\n name: \"waitForTx\",\n kind: SpanKind.CLIENT,\n spanName: () => \"idos.kwil.waitForTx\",\n attributes: (args) => ({\n [ATTR_DB_SYSTEM_NAME]: IDOS_KWIL_SYSTEM,\n [ATTR_DB_NAMESPACE]: IDOS_KWIL_NAMESPACE,\n ...(typeof args[0] === \"string\" ? { [ATTR_IDOS_KWIL_TX_HASH]: args[0] } : {}),\n }),\n },\n ],\n },\n ],\n },\n {\n name: \"@idos-network/client\",\n supportedVersions: SUPPORTED,\n classes: [\n { className: \"idOSClientConfiguration\", methods: [\"createClient\"] },\n {\n className: \"idOSClientIdle\",\n methods: [\"addressHasProfile\", \"withUserSigner\", \"logOut\"],\n staticMethods: [\"fromConfig\"],\n },\n {\n className: \"idOSClientWithUserSigner\",\n methods: [\"hasProfile\", \"createUserEncryptionProfile\", \"logIn\", \"logOut\"],\n },\n { className: \"idOSClientLoggedIn\", methods: CLIENT_LOGGED_IN_METHODS },\n ],\n },\n {\n name: \"@idos-network/issuer\",\n supportedVersions: SUPPORTED,\n classes: [\n {\n className: \"idOSIssuer\",\n staticMethods: [\"init\"],\n methods: [\n \"hasProfile\",\n \"createUserProfile\",\n \"upsertWalletAsInserter\",\n \"createUser\",\n \"getUser\",\n \"requestDelegatedWriteGrantMessage\",\n \"createCredentialByDelegatedWriteGrant\",\n \"editCredentialAsIssuer\",\n \"getCredentialIdByContentHash\",\n \"getCredentialShared\",\n ],\n },\n ],\n },\n {\n name: \"@idos-network/consumer\",\n supportedVersions: SUPPORTED,\n classes: [\n {\n className: \"idOSConsumer\",\n staticMethods: [\"init\"],\n methods: [\n \"getCredentialSharedFromIDOS\",\n \"getCredentialSharedContentDecrypted\",\n \"rescindSharedCredential\",\n \"getGrantsCount\",\n \"getAccessGrantsForCredential\",\n \"getCredentialsSharedByUser\",\n \"getAccessGrants\",\n \"verifyCredential\",\n ],\n },\n ],\n },\n];\n","// Keep in sync with package.json — it is the `otel.scope.version` of every emitted span.\nexport const PACKAGE_NAME = \"@idos-network/instrumentation\";\nexport const PACKAGE_VERSION = \"2.0.1\";\n","import type { Attributes, Span } from \"@opentelemetry/api\";\n\nimport { SpanKind, SpanStatusCode, trace } from \"@opentelemetry/api\";\nimport {\n InstrumentationBase,\n InstrumentationNodeModuleDefinition,\n isWrapped,\n} from \"@opentelemetry/instrumentation\";\nimport { ATTR_ERROR_TYPE } from \"@opentelemetry/semantic-conventions\";\n\nimport type { MethodTarget, ModuleTarget } from \"./targets\";\nimport type { IdosInstrumentationConfig } from \"./types\";\n\nimport { ATTR_IDOS_SDK_CLASS, ATTR_IDOS_SDK_METHOD, ATTR_IDOS_SDK_PACKAGE } from \"./semconv\";\nimport { TARGETS } from \"./targets\";\nimport { PACKAGE_NAME, PACKAGE_VERSION } from \"./version\";\n\ntype AnyMethod = (...args: unknown[]) => unknown;\n\nconst isThenable = (value: unknown): value is PromiseLike<unknown> =>\n typeof (value as PromiseLike<unknown> | null)?.then === \"function\";\n\n/**\n * Traces the idOS SDK: every public method of `@idos-network/client`,\n * `/consumer`, `/issuer`, the enclave providers, `verifyCredential`, and the\n * kwil `call`/`execute` round-trips they all bottom out in.\n */\nexport class IdosInstrumentation extends InstrumentationBase<IdosInstrumentationConfig> {\n constructor(config: IdosInstrumentationConfig = {}) {\n super(PACKAGE_NAME, PACKAGE_VERSION, config);\n }\n\n protected init(): InstrumentationNodeModuleDefinition[] {\n return TARGETS.map(\n (target) =>\n new InstrumentationNodeModuleDefinition(\n target.name,\n target.supportedVersions,\n (moduleExports: unknown) => this.applyPatch(target.name, target, moduleExports),\n (moduleExports: unknown) => this.removePatch(target.name, target, moduleExports),\n ),\n );\n }\n\n /**\n * Patch a module namespace you already imported.\n *\n * The automatic hooking in {@link InstrumentationBase} needs Node's module\n * loader hook (`import-in-the-middle`), which has to be installed with\n * `module.register()` before the import — and does not exist at all in a\n * browser or a bundled app. This is the alternative everywhere: import the\n * idOS module and hand its namespace over. Exports the module does not have\n * are skipped.\n */\n patchModuleExports(moduleName: string, moduleExports: unknown): void {\n const target = this.findTarget(moduleName);\n if (target) this.applyPatch(moduleName, target, moduleExports);\n }\n\n /** Reverse of {@link patchModuleExports}. */\n unpatchModuleExports(moduleName: string, moduleExports: unknown): void {\n const target = this.findTarget(moduleName);\n if (target) this.removePatch(moduleName, target, moduleExports);\n }\n\n private findTarget(moduleName: string): ModuleTarget | undefined {\n const target = TARGETS.find((t) => t.name === moduleName);\n if (!target) this._diag.warn(`No instrumentation targets known for \"${moduleName}\"`);\n return target;\n }\n\n private applyPatch(moduleName: string, target: ModuleTarget, moduleExports: unknown): unknown {\n this.eachMember(moduleName, target, moduleExports, (owner, name, method, className) => {\n if (isWrapped(owner[name])) this._unwrap(owner, name);\n this._wrap(owner, name, (original) =>\n this.traced(original as AnyMethod, moduleName, className, method),\n );\n });\n return moduleExports;\n }\n\n private removePatch(moduleName: string, target: ModuleTarget, moduleExports: unknown): unknown {\n this.eachMember(moduleName, target, moduleExports, (owner, name) => {\n if (isWrapped(owner[name])) this._unwrap(owner, name);\n });\n return moduleExports;\n }\n\n /** Resolves every target method to the object that owns it, skipping what is absent. */\n private eachMember(\n moduleName: string,\n target: ModuleTarget,\n moduleExports: unknown,\n visit: (\n owner: Record<string, unknown>,\n name: string,\n method: MethodTarget,\n className: string | undefined,\n ) => void,\n ): void {\n if (typeof moduleExports !== \"object\" || moduleExports === null) return;\n const namespace = moduleExports as Record<string, unknown>;\n\n const visitOne = (\n owner: Record<string, unknown>,\n member: string | MethodTarget,\n className: string | undefined,\n ): void => {\n const method: MethodTarget = typeof member === \"string\" ? { name: member } : member;\n const label = `${className ? `${className}.` : \"\"}${method.name}`;\n if (typeof owner[method.name] !== \"function\") {\n this._diag.debug(`${moduleName}: ${label} is not a function, skipping`);\n return;\n }\n try {\n visit(owner, method.name, method, className);\n } catch (error) {\n // An ESM namespace object is read-only unless an import hook made it writable.\n this._diag.warn(`${moduleName}: could not patch ${label}: ${String(error)}`);\n }\n };\n\n for (const klass of target.classes) {\n const ctor = namespace[klass.className];\n if (typeof ctor !== \"function\") {\n this._diag.debug(`${moduleName}: class ${klass.className} not exported, skipping`);\n continue;\n }\n const prototype = (ctor as { prototype?: unknown }).prototype;\n if (typeof prototype === \"object\" && prototype !== null) {\n for (const member of klass.methods) {\n visitOne(prototype as Record<string, unknown>, member, klass.className);\n }\n }\n for (const member of klass.staticMethods ?? []) {\n visitOne(ctor as unknown as Record<string, unknown>, member, klass.className);\n }\n }\n }\n\n private traced(\n original: AnyMethod,\n moduleName: string,\n className: string | undefined,\n target: MethodTarget,\n ): AnyMethod {\n // The patch must stay a `function` so the wrapped method keeps its call-site\n // receiver, which leaves `this` taken — same as instrumentation-pg does.\n // oxlint-disable-next-line typescript/no-this-alias -- see above\n const instrumentation = this;\n const defaultSpanName = `${className ? `${className}.` : \"\"}${target.name}`;\n\n return function patched(this: unknown, ...args: unknown[]): unknown {\n const config = instrumentation.getConfig();\n if (config.requireParentSpan && trace.getActiveSpan() === undefined) {\n return original.apply(this, args);\n }\n\n const attributes: Attributes = {\n [ATTR_IDOS_SDK_PACKAGE]: moduleName,\n [ATTR_IDOS_SDK_METHOD]: target.name,\n ...(className ? { [ATTR_IDOS_SDK_CLASS]: className } : {}),\n ...target.attributes?.(args),\n };\n\n const spanName = target.spanName?.(args) ?? defaultSpanName;\n const spanOptions = { kind: target.kind ?? SpanKind.INTERNAL, attributes };\n\n return instrumentation.tracer.startActiveSpan(spanName, spanOptions, (span) => {\n instrumentation._runSpanCustomizationHook(config.requestHook, \"requestHook\", span, {\n moduleName,\n className,\n methodName: target.name,\n args,\n });\n\n let result: unknown;\n try {\n result = original.apply(this, args);\n } catch (error) {\n instrumentation.failSpan(span, error);\n throw error;\n }\n\n if (!isThenable(result)) {\n instrumentation.endSpan(span, moduleName, className, target, result);\n return result;\n }\n\n return result.then(\n (value: unknown) => {\n instrumentation.endSpan(span, moduleName, className, target, value);\n return value;\n },\n (error: unknown) => {\n instrumentation.failSpan(span, error);\n throw error;\n },\n );\n });\n };\n }\n\n private endSpan(\n span: Span,\n moduleName: string,\n className: string | undefined,\n target: MethodTarget,\n result: unknown,\n ): void {\n const attributes = target.responseAttributes?.(result);\n if (attributes) span.setAttributes(attributes);\n\n this._runSpanCustomizationHook(this.getConfig().responseHook, \"responseHook\", span, {\n moduleName,\n className,\n methodName: target.name,\n result,\n });\n span.end();\n }\n\n private failSpan(span: Span, error: unknown): void {\n if (error instanceof Error) {\n span.recordException(error);\n span.setAttribute(ATTR_ERROR_TYPE, error.name);\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n } else {\n span.setAttribute(ATTR_ERROR_TYPE, typeof error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });\n }\n span.end();\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "@idos-network/instrumentation",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "2.2.0",
4
+ "description": "OpenTelemetry instrumentation for the idOS JavaScript SDK",
5
+ "keywords": [
6
+ "idos",
7
+ "instrumentation",
8
+ "observability",
9
+ "opentelemetry",
10
+ "tracing"
11
+ ],
12
+ "homepage": "https://idos.network",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "github:idos-network/idos-sdk-js"
17
+ },
18
+ "files": [
19
+ "dist/**/*",
20
+ "src/**/*"
21
+ ],
22
+ "type": "module",
23
+ "sideEffects": false,
24
+ "main": "./dist/index.mjs",
25
+ "module": "dist/index.mjs",
26
+ "types": "./dist/index.d.mts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.mts",
30
+ "import": "./dist/index.mjs"
31
+ }
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "@opentelemetry/instrumentation": "^0.222.0",
38
+ "@opentelemetry/semantic-conventions": "^1.43.0"
39
+ },
40
+ "devDependencies": {
41
+ "@idos-network/client": "2.2.0",
42
+ "@idos-network/consumer": "2.2.0",
43
+ "@idos-network/issuer": "2.2.0",
44
+ "@idos-network/kwil-infra": "2.2.0",
45
+ "@opentelemetry/api": "^1.9.0",
46
+ "@opentelemetry/context-async-hooks": "^2.11.0",
47
+ "@opentelemetry/sdk-trace-base": "^2.11.0",
48
+ "@types/node": "25.9.5",
49
+ "@vitest/coverage-v8": "4.1.11",
50
+ "tsdown": "^0.23.0",
51
+ "typescript": "^5.9.3",
52
+ "vitest": "4.1.11"
53
+ },
54
+ "peerDependencies": {
55
+ "@opentelemetry/api": "^1.9.0"
56
+ },
57
+ "engines": {
58
+ "node": ">=22.19.0"
59
+ },
60
+ "scripts": {
61
+ "build": "tsdown",
62
+ "dev": "tsdown --watch",
63
+ "publint": "pnpx publint --strict",
64
+ "test": "vitest",
65
+ "coverage": "vitest run --coverage",
66
+ "typecheck": "tsc --noEmit"
67
+ }
6
68
  }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ export { IdosInstrumentation } from "./instrumentation";
2
+ export {
3
+ ATTR_IDOS_KWIL_SYNCHRONOUS,
4
+ ATTR_IDOS_KWIL_TX_HASH,
5
+ ATTR_IDOS_SDK_CLASS,
6
+ ATTR_IDOS_SDK_METHOD,
7
+ ATTR_IDOS_SDK_PACKAGE,
8
+ IDOS_KWIL_NAMESPACE,
9
+ IDOS_KWIL_SYSTEM,
10
+ } from "./semconv";
11
+ export { TARGETS } from "./targets";
12
+ export type { ClassTarget, MethodTarget, ModuleTarget } from "./targets";
13
+ export type {
14
+ IdosInstrumentationConfig,
15
+ IdosInstrumentationRequestHook,
16
+ IdosInstrumentationResponseHook,
17
+ IdosRequestHookInformation,
18
+ IdosResponseHookInformation,
19
+ } from "./types";
@@ -0,0 +1,191 @@
1
+ import type { ReadableSpan } from "@opentelemetry/sdk-trace-base";
2
+
3
+ import { context, SpanKind, SpanStatusCode } from "@opentelemetry/api";
4
+ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
5
+ import {
6
+ BasicTracerProvider,
7
+ InMemorySpanExporter,
8
+ SimpleSpanProcessor,
9
+ } from "@opentelemetry/sdk-trace-base";
10
+ import {
11
+ ATTR_DB_NAMESPACE,
12
+ ATTR_DB_OPERATION_NAME,
13
+ ATTR_DB_SYSTEM_NAME,
14
+ ATTR_ERROR_TYPE,
15
+ } from "@opentelemetry/semantic-conventions";
16
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
17
+
18
+ import { IdosInstrumentation } from "./instrumentation";
19
+ import { ATTR_IDOS_KWIL_TX_HASH, ATTR_IDOS_SDK_CLASS, ATTR_IDOS_SDK_METHOD } from "./semconv";
20
+
21
+ type KwilCallParams = { name: string; inputs?: Record<string, unknown> };
22
+
23
+ /**
24
+ * Stand-ins with the shape `TARGETS` expects. Built fresh per test so that a
25
+ * patched prototype never leaks into the next one.
26
+ */
27
+ const buildNamespaces = () => {
28
+ class KwilActionClient {
29
+ async call(params: KwilCallParams): Promise<string> {
30
+ return `called:${params.name}`;
31
+ }
32
+
33
+ async execute(params: KwilCallParams): Promise<string> {
34
+ if (params.name === "boom") throw new Error("kwil exploded");
35
+ const txHash = "0xdeadbeef";
36
+ await this.waitForTx(txHash);
37
+ return txHash;
38
+ }
39
+
40
+ async waitForTx(txHash: string): Promise<void> {
41
+ if (txHash === "tx-that-times-out") {
42
+ const error = new Error("timed out waiting for tx");
43
+ error.name = "KwilTxPollTimeoutError";
44
+ throw error;
45
+ }
46
+ }
47
+ }
48
+
49
+ const kwil = new KwilActionClient();
50
+
51
+ class idOSIssuer {
52
+ async getUser(id: string): Promise<{ id: string }> {
53
+ await kwil.call({ name: "get_user", inputs: { id } });
54
+ return { id };
55
+ }
56
+ }
57
+
58
+ return {
59
+ kwil,
60
+ kwilInfra: { KwilActionClient },
61
+ issuer: { idOSIssuer },
62
+ };
63
+ };
64
+
65
+ describe("IdosInstrumentation", () => {
66
+ let exporter: InMemorySpanExporter;
67
+ let instrumentation: IdosInstrumentation;
68
+ let namespaces: ReturnType<typeof buildNamespaces>;
69
+
70
+ beforeEach(() => {
71
+ // Without a context manager `context.with` cannot propagate, so nothing nests.
72
+ context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable());
73
+ exporter = new InMemorySpanExporter();
74
+ instrumentation = new IdosInstrumentation();
75
+ instrumentation.setTracerProvider(
76
+ new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] }),
77
+ );
78
+ namespaces = buildNamespaces();
79
+ });
80
+
81
+ afterEach(() => {
82
+ context.disable();
83
+ });
84
+
85
+ const finished = (): ReadableSpan[] => exporter.getFinishedSpans();
86
+
87
+ it("traces kwil calls with db semantic conventions and keeps inputs off the span", async () => {
88
+ instrumentation.patchModuleExports("@idos-network/kwil-infra", namespaces.kwilInfra);
89
+
90
+ await namespaces.kwil.call({ name: "get_user", inputs: { id: "secret-user-id" } });
91
+
92
+ expect(finished()).toHaveLength(1);
93
+ const [span] = finished();
94
+ expect(span.name).toBe("idos.kwil.call get_user");
95
+ expect(span.kind).toBe(SpanKind.CLIENT);
96
+ expect(span.attributes[ATTR_DB_SYSTEM_NAME]).toBe("kwil");
97
+ expect(span.attributes[ATTR_DB_NAMESPACE]).toBe("main");
98
+ expect(span.attributes[ATTR_DB_OPERATION_NAME]).toBe("get_user");
99
+ expect(JSON.stringify(span.attributes)).not.toContain("secret-user-id");
100
+ });
101
+
102
+ it("records the tx hash of an execute", async () => {
103
+ instrumentation.patchModuleExports("@idos-network/kwil-infra", namespaces.kwilInfra);
104
+
105
+ await namespaces.kwil.execute({ name: "add_wallet" });
106
+
107
+ expect(finished()[0].attributes[ATTR_IDOS_KWIL_TX_HASH]).toBe("0xdeadbeef");
108
+ });
109
+
110
+ it("traces the tx wait as a child of the execute that broadcast it", async () => {
111
+ instrumentation.patchModuleExports("@idos-network/kwil-infra", namespaces.kwilInfra);
112
+
113
+ await namespaces.kwil.execute({ name: "add_wallet" });
114
+
115
+ const [wait, execute] = finished();
116
+ expect(wait.name).toBe("idos.kwil.waitForTx");
117
+ expect(wait.kind).toBe(SpanKind.CLIENT);
118
+ expect(wait.attributes[ATTR_IDOS_KWIL_TX_HASH]).toBe("0xdeadbeef");
119
+ expect(execute.name).toBe("idos.kwil.execute add_wallet");
120
+ expect(wait.parentSpanContext?.spanId).toBe(execute.spanContext().spanId);
121
+ });
122
+
123
+ it("distinguishes a tx poll timeout from other failures via error.type", async () => {
124
+ instrumentation.patchModuleExports("@idos-network/kwil-infra", namespaces.kwilInfra);
125
+
126
+ await expect(namespaces.kwil.waitForTx("tx-that-times-out")).rejects.toThrow(
127
+ "timed out waiting for tx",
128
+ );
129
+
130
+ const [span] = finished();
131
+ expect(span.status.code).toBe(SpanStatusCode.ERROR);
132
+ expect(span.attributes[ATTR_ERROR_TYPE]).toBe("KwilTxPollTimeoutError");
133
+ });
134
+
135
+ it("marks the span failed and rethrows when the method rejects", async () => {
136
+ instrumentation.patchModuleExports("@idos-network/kwil-infra", namespaces.kwilInfra);
137
+
138
+ await expect(namespaces.kwil.execute({ name: "boom" })).rejects.toThrow("kwil exploded");
139
+
140
+ const [span] = finished();
141
+ expect(span.status.code).toBe(SpanStatusCode.ERROR);
142
+ expect(span.status.message).toBe("kwil exploded");
143
+ expect(span.events.map((event) => event.name)).toContain("exception");
144
+ });
145
+
146
+ it("nests kwil spans under the SDK method that issued them", async () => {
147
+ instrumentation.patchModuleExports("@idos-network/kwil-infra", namespaces.kwilInfra);
148
+ instrumentation.patchModuleExports("@idos-network/issuer", namespaces.issuer);
149
+
150
+ await new namespaces.issuer.idOSIssuer().getUser("u1");
151
+
152
+ const [child, parent] = finished();
153
+ expect(parent.name).toBe("idOSIssuer.getUser");
154
+ expect(parent.attributes[ATTR_IDOS_SDK_CLASS]).toBe("idOSIssuer");
155
+ expect(parent.attributes[ATTR_IDOS_SDK_METHOD]).toBe("getUser");
156
+ expect(child.name).toBe("idos.kwil.call get_user");
157
+ expect(child.parentSpanContext?.spanId).toBe(parent.spanContext().spanId);
158
+ });
159
+
160
+ it("skips instrumentation when requireParentSpan is set and there is no parent", async () => {
161
+ instrumentation.setConfig({ requireParentSpan: true });
162
+ instrumentation.patchModuleExports("@idos-network/kwil-infra", namespaces.kwilInfra);
163
+
164
+ await namespaces.kwil.call({ name: "get_user" });
165
+
166
+ expect(finished()).toHaveLength(0);
167
+ });
168
+
169
+ it("runs the request and response hooks so callers can opt into their own attributes", async () => {
170
+ instrumentation.setConfig({
171
+ requestHook: (span, info) => span.setAttribute("test.args", JSON.stringify(info.args)),
172
+ responseHook: (span, info) => span.setAttribute("test.result", String(info.result)),
173
+ });
174
+ instrumentation.patchModuleExports("@idos-network/kwil-infra", namespaces.kwilInfra);
175
+
176
+ await namespaces.kwil.call({ name: "get_user", inputs: { id: "u1" } });
177
+
178
+ const [span] = finished();
179
+ expect(span.attributes["test.args"]).toContain("u1");
180
+ expect(span.attributes["test.result"]).toBe("called:get_user");
181
+ });
182
+
183
+ it("restores the original methods on unpatch", async () => {
184
+ instrumentation.patchModuleExports("@idos-network/kwil-infra", namespaces.kwilInfra);
185
+ instrumentation.unpatchModuleExports("@idos-network/kwil-infra", namespaces.kwilInfra);
186
+
187
+ await namespaces.kwil.call({ name: "get_user" });
188
+
189
+ expect(finished()).toHaveLength(0);
190
+ });
191
+ });
@@ -0,0 +1,234 @@
1
+ import type { Attributes, Span } from "@opentelemetry/api";
2
+
3
+ import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
4
+ import {
5
+ InstrumentationBase,
6
+ InstrumentationNodeModuleDefinition,
7
+ isWrapped,
8
+ } from "@opentelemetry/instrumentation";
9
+ import { ATTR_ERROR_TYPE } from "@opentelemetry/semantic-conventions";
10
+
11
+ import type { MethodTarget, ModuleTarget } from "./targets";
12
+ import type { IdosInstrumentationConfig } from "./types";
13
+
14
+ import { ATTR_IDOS_SDK_CLASS, ATTR_IDOS_SDK_METHOD, ATTR_IDOS_SDK_PACKAGE } from "./semconv";
15
+ import { TARGETS } from "./targets";
16
+ import { PACKAGE_NAME, PACKAGE_VERSION } from "./version";
17
+
18
+ type AnyMethod = (...args: unknown[]) => unknown;
19
+
20
+ const isThenable = (value: unknown): value is PromiseLike<unknown> =>
21
+ typeof (value as PromiseLike<unknown> | null)?.then === "function";
22
+
23
+ /**
24
+ * Traces the idOS SDK: every public method of `@idos-network/client`,
25
+ * `/consumer`, `/issuer`, the enclave providers, `verifyCredential`, and the
26
+ * kwil `call`/`execute` round-trips they all bottom out in.
27
+ */
28
+ export class IdosInstrumentation extends InstrumentationBase<IdosInstrumentationConfig> {
29
+ constructor(config: IdosInstrumentationConfig = {}) {
30
+ super(PACKAGE_NAME, PACKAGE_VERSION, config);
31
+ }
32
+
33
+ protected init(): InstrumentationNodeModuleDefinition[] {
34
+ return TARGETS.map(
35
+ (target) =>
36
+ new InstrumentationNodeModuleDefinition(
37
+ target.name,
38
+ target.supportedVersions,
39
+ (moduleExports: unknown) => this.applyPatch(target.name, target, moduleExports),
40
+ (moduleExports: unknown) => this.removePatch(target.name, target, moduleExports),
41
+ ),
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Patch a module namespace you already imported.
47
+ *
48
+ * The automatic hooking in {@link InstrumentationBase} needs Node's module
49
+ * loader hook (`import-in-the-middle`), which has to be installed with
50
+ * `module.register()` before the import — and does not exist at all in a
51
+ * browser or a bundled app. This is the alternative everywhere: import the
52
+ * idOS module and hand its namespace over. Exports the module does not have
53
+ * are skipped.
54
+ */
55
+ patchModuleExports(moduleName: string, moduleExports: unknown): void {
56
+ const target = this.findTarget(moduleName);
57
+ if (target) this.applyPatch(moduleName, target, moduleExports);
58
+ }
59
+
60
+ /** Reverse of {@link patchModuleExports}. */
61
+ unpatchModuleExports(moduleName: string, moduleExports: unknown): void {
62
+ const target = this.findTarget(moduleName);
63
+ if (target) this.removePatch(moduleName, target, moduleExports);
64
+ }
65
+
66
+ private findTarget(moduleName: string): ModuleTarget | undefined {
67
+ const target = TARGETS.find((t) => t.name === moduleName);
68
+ if (!target) this._diag.warn(`No instrumentation targets known for "${moduleName}"`);
69
+ return target;
70
+ }
71
+
72
+ private applyPatch(moduleName: string, target: ModuleTarget, moduleExports: unknown): unknown {
73
+ this.eachMember(moduleName, target, moduleExports, (owner, name, method, className) => {
74
+ if (isWrapped(owner[name])) this._unwrap(owner, name);
75
+ this._wrap(owner, name, (original) =>
76
+ this.traced(original as AnyMethod, moduleName, className, method),
77
+ );
78
+ });
79
+ return moduleExports;
80
+ }
81
+
82
+ private removePatch(moduleName: string, target: ModuleTarget, moduleExports: unknown): unknown {
83
+ this.eachMember(moduleName, target, moduleExports, (owner, name) => {
84
+ if (isWrapped(owner[name])) this._unwrap(owner, name);
85
+ });
86
+ return moduleExports;
87
+ }
88
+
89
+ /** Resolves every target method to the object that owns it, skipping what is absent. */
90
+ private eachMember(
91
+ moduleName: string,
92
+ target: ModuleTarget,
93
+ moduleExports: unknown,
94
+ visit: (
95
+ owner: Record<string, unknown>,
96
+ name: string,
97
+ method: MethodTarget,
98
+ className: string | undefined,
99
+ ) => void,
100
+ ): void {
101
+ if (typeof moduleExports !== "object" || moduleExports === null) return;
102
+ const namespace = moduleExports as Record<string, unknown>;
103
+
104
+ const visitOne = (
105
+ owner: Record<string, unknown>,
106
+ member: string | MethodTarget,
107
+ className: string | undefined,
108
+ ): void => {
109
+ const method: MethodTarget = typeof member === "string" ? { name: member } : member;
110
+ const label = `${className ? `${className}.` : ""}${method.name}`;
111
+ if (typeof owner[method.name] !== "function") {
112
+ this._diag.debug(`${moduleName}: ${label} is not a function, skipping`);
113
+ return;
114
+ }
115
+ try {
116
+ visit(owner, method.name, method, className);
117
+ } catch (error) {
118
+ // An ESM namespace object is read-only unless an import hook made it writable.
119
+ this._diag.warn(`${moduleName}: could not patch ${label}: ${String(error)}`);
120
+ }
121
+ };
122
+
123
+ for (const klass of target.classes) {
124
+ const ctor = namespace[klass.className];
125
+ if (typeof ctor !== "function") {
126
+ this._diag.debug(`${moduleName}: class ${klass.className} not exported, skipping`);
127
+ continue;
128
+ }
129
+ const prototype = (ctor as { prototype?: unknown }).prototype;
130
+ if (typeof prototype === "object" && prototype !== null) {
131
+ for (const member of klass.methods) {
132
+ visitOne(prototype as Record<string, unknown>, member, klass.className);
133
+ }
134
+ }
135
+ for (const member of klass.staticMethods ?? []) {
136
+ visitOne(ctor as unknown as Record<string, unknown>, member, klass.className);
137
+ }
138
+ }
139
+ }
140
+
141
+ private traced(
142
+ original: AnyMethod,
143
+ moduleName: string,
144
+ className: string | undefined,
145
+ target: MethodTarget,
146
+ ): AnyMethod {
147
+ // The patch must stay a `function` so the wrapped method keeps its call-site
148
+ // receiver, which leaves `this` taken — same as instrumentation-pg does.
149
+ // oxlint-disable-next-line typescript/no-this-alias -- see above
150
+ const instrumentation = this;
151
+ const defaultSpanName = `${className ? `${className}.` : ""}${target.name}`;
152
+
153
+ return function patched(this: unknown, ...args: unknown[]): unknown {
154
+ const config = instrumentation.getConfig();
155
+ if (config.requireParentSpan && trace.getActiveSpan() === undefined) {
156
+ return original.apply(this, args);
157
+ }
158
+
159
+ const attributes: Attributes = {
160
+ [ATTR_IDOS_SDK_PACKAGE]: moduleName,
161
+ [ATTR_IDOS_SDK_METHOD]: target.name,
162
+ ...(className ? { [ATTR_IDOS_SDK_CLASS]: className } : {}),
163
+ ...target.attributes?.(args),
164
+ };
165
+
166
+ const spanName = target.spanName?.(args) ?? defaultSpanName;
167
+ const spanOptions = { kind: target.kind ?? SpanKind.INTERNAL, attributes };
168
+
169
+ return instrumentation.tracer.startActiveSpan(spanName, spanOptions, (span) => {
170
+ instrumentation._runSpanCustomizationHook(config.requestHook, "requestHook", span, {
171
+ moduleName,
172
+ className,
173
+ methodName: target.name,
174
+ args,
175
+ });
176
+
177
+ let result: unknown;
178
+ try {
179
+ result = original.apply(this, args);
180
+ } catch (error) {
181
+ instrumentation.failSpan(span, error);
182
+ throw error;
183
+ }
184
+
185
+ if (!isThenable(result)) {
186
+ instrumentation.endSpan(span, moduleName, className, target, result);
187
+ return result;
188
+ }
189
+
190
+ return result.then(
191
+ (value: unknown) => {
192
+ instrumentation.endSpan(span, moduleName, className, target, value);
193
+ return value;
194
+ },
195
+ (error: unknown) => {
196
+ instrumentation.failSpan(span, error);
197
+ throw error;
198
+ },
199
+ );
200
+ });
201
+ };
202
+ }
203
+
204
+ private endSpan(
205
+ span: Span,
206
+ moduleName: string,
207
+ className: string | undefined,
208
+ target: MethodTarget,
209
+ result: unknown,
210
+ ): void {
211
+ const attributes = target.responseAttributes?.(result);
212
+ if (attributes) span.setAttributes(attributes);
213
+
214
+ this._runSpanCustomizationHook(this.getConfig().responseHook, "responseHook", span, {
215
+ moduleName,
216
+ className,
217
+ methodName: target.name,
218
+ result,
219
+ });
220
+ span.end();
221
+ }
222
+
223
+ private failSpan(span: Span, error: unknown): void {
224
+ if (error instanceof Error) {
225
+ span.recordException(error);
226
+ span.setAttribute(ATTR_ERROR_TYPE, error.name);
227
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
228
+ } else {
229
+ span.setAttribute(ATTR_ERROR_TYPE, typeof error);
230
+ span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
231
+ }
232
+ span.end();
233
+ }
234
+ }
package/src/semconv.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Attributes emitted by this instrumentation that have no stable OpenTelemetry
3
+ * semantic convention. Stable ones (`db.*`, `error.type`) are imported from
4
+ * `@opentelemetry/semantic-conventions` at the call site.
5
+ */
6
+
7
+ /** Package the traced method lives in, e.g. `@idos-network/issuer`. */
8
+ export const ATTR_IDOS_SDK_PACKAGE = "idos.sdk.package";
9
+
10
+ /** Class the traced method lives on, e.g. `idOSIssuer`. */
11
+ export const ATTR_IDOS_SDK_CLASS = "idos.sdk.class";
12
+
13
+ /** Traced method name, e.g. `createUserProfile`. */
14
+ export const ATTR_IDOS_SDK_METHOD = "idos.sdk.method";
15
+
16
+ /** `db.system.name` value for the idOS kwil network. */
17
+ export const IDOS_KWIL_SYSTEM = "kwil";
18
+
19
+ /** Kwil namespace every idOS action runs against. */
20
+ export const IDOS_KWIL_NAMESPACE = "main";
21
+
22
+ /** Transaction hash returned by a kwil `execute`. */
23
+ export const ATTR_IDOS_KWIL_TX_HASH = "idos.kwil.tx_hash";
24
+
25
+ /** Whether a kwil `execute` waited for the transaction to be mined. */
26
+ export const ATTR_IDOS_KWIL_SYNCHRONOUS = "idos.kwil.synchronous";
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import type { ClassTarget, MethodTarget } from "./targets";
4
+
5
+ import { TARGETS } from "./targets";
6
+
7
+ /**
8
+ * The target table names classes and methods as strings, so a rename in an SDK
9
+ * package would silently stop producing spans. This resolves every entry
10
+ * against the real modules so drift fails the build instead.
11
+ */
12
+
13
+ const unresolved = (namespace: Record<string, unknown>, classes: ClassTarget[]): string[] => {
14
+ const problems: string[] = [];
15
+
16
+ const probe = (owner: unknown, members: (string | MethodTarget)[], label: string): void => {
17
+ for (const member of members) {
18
+ const name = typeof member === "string" ? member : member.name;
19
+ if (typeof (owner as Record<string, unknown> | null)?.[name] !== "function") {
20
+ problems.push(`${label}${name}`);
21
+ }
22
+ }
23
+ };
24
+
25
+ for (const klass of classes) {
26
+ const ctor = namespace[klass.className] as { prototype?: unknown } | undefined;
27
+ if (typeof ctor !== "function") {
28
+ problems.push(klass.className);
29
+ continue;
30
+ }
31
+ probe(ctor.prototype, klass.methods, `${klass.className}.`);
32
+ probe(ctor, klass.staticMethods ?? [], `${klass.className}.`);
33
+ }
34
+
35
+ return problems;
36
+ };
37
+
38
+ describe("TARGETS", () => {
39
+ for (const target of TARGETS) {
40
+ it(`resolves every target in ${target.name}`, async () => {
41
+ const namespace = (await import(target.name)) as Record<string, unknown>;
42
+ expect(unresolved(namespace, target.classes)).toEqual([]);
43
+ });
44
+ }
45
+
46
+ // Every method has to sit on a prototype or constructor for the no-loader-hook
47
+ // path to reach it; a bare function export would need `import-in-the-middle`.
48
+ it("has no bare function exports as targets", () => {
49
+ expect(TARGETS.every((t) => t.classes.length > 0)).toBe(true);
50
+ });
51
+ });
package/src/targets.ts ADDED
@@ -0,0 +1,209 @@
1
+ import type { Attributes } from "@opentelemetry/api";
2
+
3
+ import { SpanKind } from "@opentelemetry/api";
4
+ import {
5
+ ATTR_DB_NAMESPACE,
6
+ ATTR_DB_OPERATION_NAME,
7
+ ATTR_DB_SYSTEM_NAME,
8
+ } from "@opentelemetry/semantic-conventions";
9
+
10
+ import {
11
+ ATTR_IDOS_KWIL_SYNCHRONOUS,
12
+ ATTR_IDOS_KWIL_TX_HASH,
13
+ IDOS_KWIL_NAMESPACE,
14
+ IDOS_KWIL_SYSTEM,
15
+ } from "./semconv";
16
+
17
+ /** A method to wrap. A bare string uses every default. */
18
+ export type MethodTarget = {
19
+ name: string;
20
+ /** Span name. Defaults to `<Class>.<method>`, or `<method>` for a function export. */
21
+ spanName?: (args: readonly unknown[]) => string;
22
+ /** Attributes derived from the arguments. Must not emit personal data. */
23
+ attributes?: (args: readonly unknown[]) => Attributes;
24
+ /** Attributes derived from the resolved value. Must not emit personal data. */
25
+ responseAttributes?: (result: unknown) => Attributes;
26
+ /** @default SpanKind.INTERNAL */
27
+ kind?: SpanKind;
28
+ };
29
+
30
+ export type ClassTarget = {
31
+ /** Name of the class as exported by the module. */
32
+ className: string;
33
+ /** Instance methods, wrapped on the prototype. */
34
+ methods: (string | MethodTarget)[];
35
+ /** Static methods, wrapped on the constructor (`init`-style factories). */
36
+ staticMethods?: (string | MethodTarget)[];
37
+ };
38
+
39
+ export type ModuleTarget = {
40
+ /** Module specifier. */
41
+ name: string;
42
+ supportedVersions: string[];
43
+ /**
44
+ * Only classes. Every method lives on a prototype or a constructor, both of
45
+ * which are ordinary mutable objects — that is what lets
46
+ * {@link IdosInstrumentation.patchModuleExports} work without a loader hook.
47
+ * A bare function export would sit on the sealed module namespace instead,
48
+ * and could only be reached via `import-in-the-middle`.
49
+ */
50
+ classes: ClassTarget[];
51
+ };
52
+
53
+ const SUPPORTED = [">=2.0.0 <3"];
54
+
55
+ /**
56
+ * Kwil `call`/`execute` are the idOS equivalent of `pg.Client.query`: every SDK
57
+ * operation bottoms out here. The action name is a fixed identifier from the
58
+ * action schema, so it is safe to record; the inputs are not.
59
+ */
60
+ const kwilAttributes = (args: readonly unknown[]): Attributes => {
61
+ const params = args[0] as { name?: unknown } | undefined;
62
+ return {
63
+ [ATTR_DB_SYSTEM_NAME]: IDOS_KWIL_SYSTEM,
64
+ [ATTR_DB_NAMESPACE]: IDOS_KWIL_NAMESPACE,
65
+ ...(typeof params?.name === "string" ? { [ATTR_DB_OPERATION_NAME]: params.name } : {}),
66
+ };
67
+ };
68
+
69
+ const kwilSpanName =
70
+ (operation: string) =>
71
+ (args: readonly unknown[]): string => {
72
+ const params = args[0] as { name?: unknown } | undefined;
73
+ return typeof params?.name === "string"
74
+ ? `idos.kwil.${operation} ${params.name}`
75
+ : `idos.kwil.${operation}`;
76
+ };
77
+
78
+ /** Methods shared by every logged-out-capable client state. */
79
+ const CLIENT_LOGGED_IN_METHODS = [
80
+ "requestDWGMessage",
81
+ "createCredential",
82
+ "removeCredential",
83
+ "getCredentialById",
84
+ "shareCredential",
85
+ "getAllCredentials",
86
+ "getAccessGrantsOwned",
87
+ "getAttributes",
88
+ "createAttribute",
89
+ "getCredentialContentSha256Hash",
90
+ "getCredentialContent",
91
+ "getCredentialWithEncryptedContent",
92
+ "getCredentialSharedContent",
93
+ "getGrants",
94
+ "getGrantsCount",
95
+ "getCredentialShared",
96
+ "revokeAccessGrant",
97
+ "addWallet",
98
+ "addWallets",
99
+ "getWallets",
100
+ "removeWallet",
101
+ "removeWallets",
102
+ "filterCredentials",
103
+ "requestAccessGrant",
104
+ "logOut",
105
+ ];
106
+
107
+ export const TARGETS: ModuleTarget[] = [
108
+ {
109
+ name: "@idos-network/kwil-infra",
110
+ supportedVersions: SUPPORTED,
111
+ classes: [
112
+ {
113
+ className: "KwilActionClient",
114
+ methods: [
115
+ {
116
+ name: "call",
117
+ kind: SpanKind.CLIENT,
118
+ spanName: kwilSpanName("call"),
119
+ attributes: kwilAttributes,
120
+ },
121
+ {
122
+ name: "execute",
123
+ kind: SpanKind.CLIENT,
124
+ spanName: kwilSpanName("execute"),
125
+ attributes: (args) => ({
126
+ ...kwilAttributes(args),
127
+ // `synchronous` defaults to true and also decides whether the span
128
+ // covers the tx-confirmation polling loop.
129
+ [ATTR_IDOS_KWIL_SYNCHRONOUS]: args[2] === undefined ? true : Boolean(args[2]),
130
+ }),
131
+ responseAttributes: (result) =>
132
+ typeof result === "string" ? { [ATTR_IDOS_KWIL_TX_HASH]: result } : {},
133
+ },
134
+ {
135
+ // Usually the bulk of an `execute`: polling the node until the tx is
136
+ // mined. A tx hash is a public chain identifier, so it is safe to record.
137
+ name: "waitForTx",
138
+ kind: SpanKind.CLIENT,
139
+ spanName: () => "idos.kwil.waitForTx",
140
+ attributes: (args) => ({
141
+ [ATTR_DB_SYSTEM_NAME]: IDOS_KWIL_SYSTEM,
142
+ [ATTR_DB_NAMESPACE]: IDOS_KWIL_NAMESPACE,
143
+ ...(typeof args[0] === "string" ? { [ATTR_IDOS_KWIL_TX_HASH]: args[0] } : {}),
144
+ }),
145
+ },
146
+ ],
147
+ },
148
+ ],
149
+ },
150
+ {
151
+ name: "@idos-network/client",
152
+ supportedVersions: SUPPORTED,
153
+ classes: [
154
+ { className: "idOSClientConfiguration", methods: ["createClient"] },
155
+ {
156
+ className: "idOSClientIdle",
157
+ methods: ["addressHasProfile", "withUserSigner", "logOut"],
158
+ staticMethods: ["fromConfig"],
159
+ },
160
+ {
161
+ className: "idOSClientWithUserSigner",
162
+ methods: ["hasProfile", "createUserEncryptionProfile", "logIn", "logOut"],
163
+ },
164
+ { className: "idOSClientLoggedIn", methods: CLIENT_LOGGED_IN_METHODS },
165
+ ],
166
+ },
167
+ {
168
+ name: "@idos-network/issuer",
169
+ supportedVersions: SUPPORTED,
170
+ classes: [
171
+ {
172
+ className: "idOSIssuer",
173
+ staticMethods: ["init"],
174
+ methods: [
175
+ "hasProfile",
176
+ "createUserProfile",
177
+ "upsertWalletAsInserter",
178
+ "createUser",
179
+ "getUser",
180
+ "requestDelegatedWriteGrantMessage",
181
+ "createCredentialByDelegatedWriteGrant",
182
+ "editCredentialAsIssuer",
183
+ "getCredentialIdByContentHash",
184
+ "getCredentialShared",
185
+ ],
186
+ },
187
+ ],
188
+ },
189
+ {
190
+ name: "@idos-network/consumer",
191
+ supportedVersions: SUPPORTED,
192
+ classes: [
193
+ {
194
+ className: "idOSConsumer",
195
+ staticMethods: ["init"],
196
+ methods: [
197
+ "getCredentialSharedFromIDOS",
198
+ "getCredentialSharedContentDecrypted",
199
+ "rescindSharedCredential",
200
+ "getGrantsCount",
201
+ "getAccessGrantsForCredential",
202
+ "getCredentialsSharedByUser",
203
+ "getAccessGrants",
204
+ "verifyCredential",
205
+ ],
206
+ },
207
+ ],
208
+ },
209
+ ];
package/src/types.ts ADDED
@@ -0,0 +1,61 @@
1
+ import type { Span } from "@opentelemetry/api";
2
+ import type { InstrumentationConfig } from "@opentelemetry/instrumentation";
3
+
4
+ /** Data about the call that is about to be made, passed to {@link IdosInstrumentationConfig.requestHook}. */
5
+ export type IdosRequestHookInformation = {
6
+ /** Module the traced method was exported from, e.g. `@idos-network/issuer`. */
7
+ moduleName: string;
8
+ /** Class the method lives on, or `undefined` for a bare function export. */
9
+ className: string | undefined;
10
+ /** Method name, e.g. `createUserProfile`. */
11
+ methodName: string;
12
+ /** The arguments the method was called with. May contain personal data — see README. */
13
+ args: readonly unknown[];
14
+ };
15
+
16
+ /** Data about the completed call, passed to {@link IdosInstrumentationConfig.responseHook}. */
17
+ export type IdosResponseHookInformation = {
18
+ moduleName: string;
19
+ className: string | undefined;
20
+ methodName: string;
21
+ /** The resolved return value. May contain personal data — see README. */
22
+ result: unknown;
23
+ };
24
+
25
+ export type IdosInstrumentationRequestHook = (span: Span, info: IdosRequestHookInformation) => void;
26
+
27
+ export type IdosInstrumentationResponseHook = (
28
+ span: Span,
29
+ info: IdosResponseHookInformation,
30
+ ) => void;
31
+
32
+ export interface IdosInstrumentationConfig extends InstrumentationConfig {
33
+ /**
34
+ * Hook for adding custom attributes from the call arguments, or renaming the span.
35
+ *
36
+ * Nothing derived from arguments is recorded by default: idOS SDK arguments carry
37
+ * credentials, wallet identifiers and encryption keys. Opt in here, per deployment.
38
+ *
39
+ * @default undefined
40
+ */
41
+ requestHook?: IdosInstrumentationRequestHook;
42
+
43
+ /**
44
+ * Hook for adding custom attributes from the resolved return value.
45
+ *
46
+ * Same caveat as {@link requestHook}: return values carry credential contents.
47
+ *
48
+ * @default undefined
49
+ */
50
+ responseHook?: IdosInstrumentationResponseHook;
51
+
52
+ /**
53
+ * If true, spans are only created when there is already an active span.
54
+ *
55
+ * Useful in the browser, where an idOS call outside a user-interaction span
56
+ * would otherwise produce a single-span orphan trace.
57
+ *
58
+ * @default false
59
+ */
60
+ requireParentSpan?: boolean;
61
+ }
package/src/version.ts ADDED
@@ -0,0 +1,3 @@
1
+ // Keep in sync with package.json — it is the `otel.scope.version` of every emitted span.
2
+ export const PACKAGE_NAME = "@idos-network/instrumentation";
3
+ export const PACKAGE_VERSION = "2.0.1";