diff --git a/packages/opentelemetry-instrumentation-xml-http-request/src/xhr.ts b/packages/opentelemetry-instrumentation-xml-http-request/src/xhr.ts
index ab9f4f4fa93..fb7274cdcea 100644
--- a/packages/opentelemetry-instrumentation-xml-http-request/src/xhr.ts
+++ b/packages/opentelemetry-instrumentation-xml-http-request/src/xhr.ts
@@ -19,6 +19,7 @@ import {
   isWrapped,
   InstrumentationBase,
   InstrumentationConfig,
+  safeExecuteInTheMiddle,
 } from '@opentelemetry/instrumentation';
 import { hrTime, isUrlIgnored, otperformance } from '@opentelemetry/core';
 import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
@@ -45,6 +46,11 @@ import { AttributeNames } from './enums/AttributeNames';
 // safe enough
 const OBSERVER_WAIT_TIME_MS = 300;
 
+export type XHRCustomAttributeFunction = (
+  span: api.Span,
+  xhr: XMLHttpRequest
+) => void;
+
 /**
  * XMLHttpRequest config
  */
@@ -64,6 +70,8 @@ export interface XMLHttpRequestInstrumentationConfig
    * also not be traced.
    */
   ignoreUrls?: Array<string | RegExp>;
+  /** Function for adding custom attributes on the span */
+  applyCustomAttributesOnSpan?: XHRCustomAttributeFunction;
 }
 
 /**
@@ -171,6 +179,24 @@ export class XMLHttpRequestInstrumentation extends InstrumentationBase<XMLHttpRe
     }
   }
 
+  private _applyAttributesAfterXHR(span: api.Span, xhr: XMLHttpRequest) {
+    const applyCustomAttributesOnSpan = this._getConfig()
+      .applyCustomAttributesOnSpan;
+    if (typeof applyCustomAttributesOnSpan === 'function') {
+      safeExecuteInTheMiddle(
+        () => applyCustomAttributesOnSpan(span, xhr),
+        error => {
+          if (!error) {
+            return;
+          }
+
+          api.diag.error('applyCustomAttributesOnSpan', error);
+        },
+        true
+      );
+    }
+  }
+
   /**
    * will collect information about all resources created
    * between "send" and "end" with additional waiting for main resource
@@ -398,6 +424,10 @@ export class XMLHttpRequestInstrumentation extends InstrumentationBase<XMLHttpRe
       xhrMem.status = xhr.status;
       xhrMem.statusText = xhr.statusText;
       plugin._xhrMem.delete(xhr);
+
+      if (xhrMem.span) {
+        plugin._applyAttributesAfterXHR(xhrMem.span, xhr);
+      }
       const endTime = hrTime();
 
       // the timeout is needed as observer doesn't have yet information
diff --git a/packages/opentelemetry-instrumentation-xml-http-request/test/xhr.test.ts b/packages/opentelemetry-instrumentation-xml-http-request/test/xhr.test.ts
index d9818bb9c0d..fa8bb22469a 100644
--- a/packages/opentelemetry-instrumentation-xml-http-request/test/xhr.test.ts
+++ b/packages/opentelemetry-instrumentation-xml-http-request/test/xhr.test.ts
@@ -34,7 +34,10 @@ import {
 import * as assert from 'assert';
 import * as sinon from 'sinon';
 import { EventNames } from '../src/enums/EventNames';
-import { XMLHttpRequestInstrumentation } from '../src/xhr';
+import {
+  XMLHttpRequestInstrumentation,
+  XMLHttpRequestInstrumentationConfig,
+} from '../src/xhr';
 import { AttributeNames } from '../src/enums/AttributeNames';
 
 class DummySpanExporter implements tracing.SpanExporter {
@@ -168,7 +171,11 @@ describe('xhr', () => {
           sinon.restore();
         };
 
-        prepareData = (done: any, fileUrl: string, config?: any) => {
+        prepareData = (
+          done: any,
+          fileUrl: string,
+          config?: XMLHttpRequestInstrumentationConfig
+        ) => {
           const fakeXhr = sinon.useFakeXMLHttpRequest();
           fakeXhr.onCreate = function (xhr: any) {
             requests.push(xhr);
@@ -698,6 +705,29 @@ describe('xhr', () => {
             );
           });
         });
+
+        describe('and applyCustomAttributesOnSpan hook is configured', () => {
+          beforeEach(done => {
+            clearData();
+            const propagateTraceHeaderCorsUrls = [url];
+            prepareData(done, url, {
+              propagateTraceHeaderCorsUrls,
+              applyCustomAttributesOnSpan: function (
+                span: api.Span,
+                xhr: XMLHttpRequest
+              ) {
+                const res = JSON.parse(xhr.response);
+                span.setAttribute('xhr-custom-attribute', res.foo);
+              },
+            });
+          });
+
+          it('span should have custom attribute', () => {
+            const span: tracing.ReadableSpan = exportSpy.args[1][0][0];
+            const attributes = span.attributes;
+            assert.ok(attributes['xhr-custom-attribute'] === 'bar');
+          });
+        });
       });
 
       describe('when request is NOT successful', () => {
@@ -711,7 +741,9 @@ describe('xhr', () => {
           'https://raw.githubusercontent.com/open-telemetry/opentelemetry-js/master/package.json';
         let fakeNow = 0;
 
-        beforeEach(() => {
+        const prepareData = function (
+          config: XMLHttpRequestInstrumentationConfig = {}
+        ) {
           const fakeXhr = sinon.useFakeXMLHttpRequest();
           fakeXhr.onCreate = function (xhr: any) {
             requests.push(xhr);
@@ -738,7 +770,7 @@ describe('xhr', () => {
           webTracerWithZoneProvider = new WebTracerProvider();
 
           registerInstrumentations({
-            instrumentations: [new XMLHttpRequestInstrumentation()],
+            instrumentations: [new XMLHttpRequestInstrumentation(config)],
             tracerProvider: webTracerWithZoneProvider,
           });
 
@@ -750,37 +782,89 @@ describe('xhr', () => {
           webTracerWithZone = webTracerWithZoneProvider.getTracer('xhr-test');
 
           rootSpan = webTracerWithZone.startSpan('root');
+        };
+
+        beforeEach(() => {
+          prepareData();
         });
 
         afterEach(() => {
           clearData();
         });
 
-        describe('when request loads and receives an error code', () => {
-          beforeEach(done => {
-            api.context.with(
-              api.setSpan(api.context.active(), rootSpan),
+        function timedOutRequest(done: any) {
+          api.context.with(api.setSpan(api.context.active(), rootSpan), () => {
+            void getData(
+              new XMLHttpRequest(),
+              url,
               () => {
-                getData(
-                  new XMLHttpRequest(),
-                  url,
-                  () => {
-                    fakeNow = 100;
-                  },
-                  testAsync
-                ).then(() => {
-                  fakeNow = 0;
-                  sinon.clock.tick(1000);
-                  done();
-                });
-                assert.strictEqual(requests.length, 1, 'request not called');
-                requests[0].respond(
-                  400,
-                  { 'Content-Type': 'text/plain' },
-                  'Bad Request'
-                );
+                sinon.clock.tick(XHR_TIMEOUT);
+              },
+              testAsync
+            ).then(() => {
+              fakeNow = 0;
+              sinon.clock.tick(1000);
+              done();
+            });
+          });
+        }
+
+        function abortedRequest(done: any) {
+          api.context.with(api.setSpan(api.context.active(), rootSpan), () => {
+            void getData(new XMLHttpRequest(), url, () => {}, testAsync).then(
+              () => {
+                fakeNow = 0;
+                sinon.clock.tick(1000);
+                done();
               }
             );
+
+            assert.strictEqual(requests.length, 1, 'request not called');
+            requests[0].abort();
+          });
+        }
+
+        function erroredRequest(done: any) {
+          api.context.with(api.setSpan(api.context.active(), rootSpan), () => {
+            void getData(
+              new XMLHttpRequest(),
+              url,
+              () => {
+                fakeNow = 100;
+              },
+              testAsync
+            ).then(() => {
+              fakeNow = 0;
+              sinon.clock.tick(1000);
+              done();
+            });
+            assert.strictEqual(requests.length, 1, 'request not called');
+            requests[0].respond(
+              400,
+              { 'Content-Type': 'text/plain' },
+              'Bad Request'
+            );
+          });
+        }
+
+        function networkErrorRequest(done: any) {
+          api.context.with(api.setSpan(api.context.active(), rootSpan), () => {
+            void getData(new XMLHttpRequest(), url, () => {}, testAsync).then(
+              () => {
+                fakeNow = 0;
+                sinon.clock.tick(1000);
+                done();
+              }
+            );
+
+            assert.strictEqual(requests.length, 1, 'request not called');
+            requests[0].error();
+          });
+        }
+
+        describe('when request loads and receives an error code', () => {
+          beforeEach(done => {
+            erroredRequest(done);
           });
           it('span should have correct attributes', () => {
             const span: tracing.ReadableSpan = exportSpy.args[0][0][0];
@@ -900,21 +984,7 @@ describe('xhr', () => {
 
         describe('when request encounters a network error', () => {
           beforeEach(done => {
-            api.context.with(
-              api.setSpan(api.context.active(), rootSpan),
-              () => {
-                getData(new XMLHttpRequest(), url, () => {}, testAsync).then(
-                  () => {
-                    fakeNow = 0;
-                    sinon.clock.tick(1000);
-                    done();
-                  }
-                );
-
-                assert.strictEqual(requests.length, 1, 'request not called');
-                requests[0].error();
-              }
-            );
+            networkErrorRequest(done);
           });
 
           it('span should have correct attributes', () => {
@@ -992,21 +1062,7 @@ describe('xhr', () => {
           });
 
           beforeEach(done => {
-            api.context.with(
-              api.setSpan(api.context.active(), rootSpan),
-              () => {
-                getData(new XMLHttpRequest(), url, () => {}, testAsync).then(
-                  () => {
-                    fakeNow = 0;
-                    sinon.clock.tick(1000);
-                    done();
-                  }
-                );
-
-                assert.strictEqual(requests.length, 1, 'request not called');
-                requests[0].abort();
-              }
-            );
+            abortedRequest(done);
           });
 
           it('span should have correct attributes', () => {
@@ -1084,23 +1140,7 @@ describe('xhr', () => {
           });
 
           beforeEach(done => {
-            api.context.with(
-              api.setSpan(api.context.active(), rootSpan),
-              () => {
-                getData(
-                  new XMLHttpRequest(),
-                  url,
-                  () => {
-                    sinon.clock.tick(XHR_TIMEOUT);
-                  },
-                  testAsync
-                ).then(() => {
-                  fakeNow = 0;
-                  sinon.clock.tick(1000);
-                  done();
-                });
-              }
-            );
+            timedOutRequest(done);
           });
 
           it('span should have correct attributes', () => {
@@ -1168,6 +1208,94 @@ describe('xhr', () => {
             assert.strictEqual(events.length, 3, 'number of events is wrong');
           });
         });
+
+        describe('when applyCustomAttributesOnSpan hook is present', () => {
+          describe('AND request loads and receives an error code', () => {
+            beforeEach(done => {
+              clearData();
+              prepareData({
+                applyCustomAttributesOnSpan: function (span, xhr) {
+                  span.setAttribute('xhr-custom-error-code', xhr.status);
+                },
+              });
+              erroredRequest(done);
+            });
+
+            it('span should have custom attribute', () => {
+              const span: tracing.ReadableSpan = exportSpy.args[0][0][0];
+              const attributes = span.attributes;
+              assert.ok(attributes['xhr-custom-error-code'] === 400);
+            });
+          });
+
+          describe('AND request encounters a network error', () => {
+            beforeEach(done => {
+              clearData();
+              prepareData({
+                applyCustomAttributesOnSpan: function (span, xhr) {
+                  span.setAttribute('xhr-custom-error-code', xhr.status);
+                },
+              });
+              networkErrorRequest(done);
+            });
+
+            it('span should have custom attribute', () => {
+              const span: tracing.ReadableSpan = exportSpy.args[0][0][0];
+              const attributes = span.attributes;
+              assert.ok(attributes['xhr-custom-error-code'] === 0);
+            });
+          });
+
+          describe('AND request is aborted', () => {
+            before(function () {
+              // Can only abort Async requests
+              if (!testAsync) {
+                this.skip();
+              }
+            });
+
+            beforeEach(done => {
+              clearData();
+              prepareData({
+                applyCustomAttributesOnSpan: function (span, xhr) {
+                  span.setAttribute('xhr-custom-error-code', xhr.status);
+                },
+              });
+              abortedRequest(done);
+            });
+
+            it('span should have custom attribute', () => {
+              const span: tracing.ReadableSpan = exportSpy.args[0][0][0];
+              const attributes = span.attributes;
+              assert.ok(attributes['xhr-custom-error-code'] === 0);
+            });
+          });
+
+          describe('AND request times out', () => {
+            before(function () {
+              // Can only set timeout for Async requests
+              if (!testAsync) {
+                this.skip();
+              }
+            });
+
+            beforeEach(done => {
+              clearData();
+              prepareData({
+                applyCustomAttributesOnSpan: function (span, xhr) {
+                  span.setAttribute('xhr-custom-error-code', xhr.status);
+                },
+              });
+              timedOutRequest(done);
+            });
+
+            it('span should have custom attribute', () => {
+              const span: tracing.ReadableSpan = exportSpy.args[0][0][0];
+              const attributes = span.attributes;
+              assert.ok(attributes['xhr-custom-error-code'] === 0);
+            });
+          });
+        });
       });
     });
   });